Misskeyでプッシュ通知の動作確認を行った。
This commit is contained in:
parent
ecbed39f5b
commit
995e7a7504
|
@ -46,6 +46,5 @@ dependencies {
|
|||
api project(":apng")
|
||||
implementation project(":base")
|
||||
|
||||
implementation "com.github.zjupure:webpdecoder:2.3.$glideVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
}
|
||||
|
|
|
@ -21,3 +21,7 @@
|
|||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-dontobfuscate
|
||||
|
||||
-keep public class com.bumptech.glide.integration.webp.WebpImage { *; }
|
||||
-keep public class com.bumptech.glide.integration.webp.WebpFrame { *; }
|
||||
-keep public class com.bumptech.glide.integration.webp.WebpBitmapFactory { *; }
|
||||
|
|
|
@ -68,6 +68,12 @@ android {
|
|||
lintOptions {
|
||||
disable "MissingTranslation"
|
||||
}
|
||||
// You need to specify either an absolute path or include the
|
||||
// keystore file in the same directory as the build.gradle file.
|
||||
storeFile file("D:\\GoogleDrive\\_private\\AndroidSignKeys\\SubwayTooter.jks")
|
||||
storePassword "password"
|
||||
keyAlias "my-alias"
|
||||
keyPassword "password"
|
||||
}
|
||||
debug {
|
||||
|
||||
|
|
|
@ -60,6 +60,10 @@
|
|||
kotlinx.serialization.KSerializer serializer(...);
|
||||
}
|
||||
|
||||
-keep public class com.bumptech.glide.integration.webp.WebpImage { *; }
|
||||
-keep public class com.bumptech.glide.integration.webp.WebpFrame { *; }
|
||||
-keep public class com.bumptech.glide.integration.webp.WebpBitmapFactory { *; }
|
||||
|
||||
# keep everything
|
||||
-keep class ** { *; }
|
||||
-keepclassmembers class ** { *** Companion; }
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package jp.juggler.subwaytooter.api
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccount.Companion.tootAccount
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRef
|
||||
import jp.juggler.subwaytooter.api.entity.TootNotification.Companion.tootNotification
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.data.*
|
||||
import org.junit.Assert.assertEquals
|
||||
|
@ -36,7 +40,7 @@ class TestDuplicateMap {
|
|||
if (url != null) put("url", url)
|
||||
}
|
||||
|
||||
return TootStatus(parser, itemJson)
|
||||
return tootStatus(parser, itemJson)
|
||||
}
|
||||
|
||||
private fun testDuplicateStatus(): ArrayList<TimelineItem> {
|
||||
|
@ -63,7 +67,7 @@ class TestDuplicateMap {
|
|||
put("url", "http://${parser.apiHost}/@user1")
|
||||
}
|
||||
|
||||
val account1 = TootAccount(parser, account1Json)
|
||||
val account1 = tootAccount(parser, account1Json)
|
||||
assertNotNull(account1)
|
||||
|
||||
val map = DuplicateMap()
|
||||
|
@ -122,7 +126,7 @@ class TestDuplicateMap {
|
|||
put("id", id)
|
||||
}
|
||||
|
||||
val item = TootNotification(parser, itemJson)
|
||||
val item = tootNotification(parser, itemJson)
|
||||
assertNotNull(item)
|
||||
generatedItems.add(item)
|
||||
assertEquals(false, map.isDuplicate(item))
|
||||
|
@ -178,7 +182,7 @@ class TestDuplicateMap {
|
|||
put("url", "http://${parser.apiHost}/@user$id")
|
||||
}
|
||||
|
||||
val item = TootAccountRef.notNull(parser, TootAccount(parser, itemJson))
|
||||
val item = tootAccountRef(parser, tootAccount(parser, itemJson))
|
||||
assertNotNull(item)
|
||||
generatedItems.add(item)
|
||||
assertEquals(false, map.isDuplicate(item))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.data.*
|
||||
|
@ -30,42 +30,42 @@ class TestEntityUtils {
|
|||
|
||||
@Test
|
||||
fun testParseItem() {
|
||||
assertEquals(null, parseItem(::TestEntity, null))
|
||||
assertEquals(null, parseItem(null, ::TestEntity))
|
||||
|
||||
run {
|
||||
val src = """{"s":null,"l":"100"}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, src)
|
||||
val item = parseItem(src, ::TestEntity)
|
||||
assertNull(item)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"","l":"100"}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, src)
|
||||
val item = parseItem(src, ::TestEntity)
|
||||
assertNull(item)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"A","l":null}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, src)
|
||||
val item = parseItem(src, ::TestEntity)
|
||||
assertNotNull(item)
|
||||
assertEquals(src.optString("s"), item?.s)
|
||||
assertEquals(src.optLong("l"), item?.l)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"A","l":""}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, src)
|
||||
val item = parseItem(src, ::TestEntity)
|
||||
assertNotNull(item)
|
||||
assertEquals(src.optString("s"), item?.s)
|
||||
assertEquals(src.optLong("l"), item?.l)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"A","l":100}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, src)
|
||||
val item = parseItem(src, ::TestEntity)
|
||||
assertNotNull(item)
|
||||
assertEquals(src.optString("s"), item?.s)
|
||||
assertEquals(src.optLong("l"), item?.l)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"A","l":"100"}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, src)
|
||||
val item = parseItem(src, ::TestEntity)
|
||||
assertNotNull(item)
|
||||
assertEquals(src.optString("s"), item?.s)
|
||||
assertEquals(src.optLong("l"), item?.l)
|
||||
|
@ -74,74 +74,74 @@ class TestEntityUtils {
|
|||
|
||||
@Test
|
||||
fun testParseList() {
|
||||
assertEquals(0, parseList(::TestEntity, null).size)
|
||||
assertEquals(0, parseList(null, ::TestEntity).size)
|
||||
|
||||
val src = JsonArray()
|
||||
assertEquals(0, parseList(::TestEntity, src).size)
|
||||
assertEquals(0, parseList(src, ::TestEntity).size)
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(1, parseList(::TestEntity, src).size)
|
||||
assertEquals(1, parseList(src, ::TestEntity).size)
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseList(::TestEntity, src).size)
|
||||
assertEquals(2, parseList(src, ::TestEntity).size)
|
||||
|
||||
// error
|
||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseList(::TestEntity, src).size)
|
||||
assertEquals(2, parseList(src, ::TestEntity).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseListOrNull() {
|
||||
assertEquals(null, parseListOrNull(::TestEntity, null))
|
||||
assertEquals(null, parseListOrNull(null, ::TestEntity))
|
||||
|
||||
val src = JsonArray()
|
||||
assertEquals(null, parseListOrNull(::TestEntity, src))
|
||||
assertEquals(null, parseListOrNull(src, ::TestEntity))
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(1, parseListOrNull(::TestEntity, src)?.size)
|
||||
assertEquals(1, parseListOrNull(src, ::TestEntity)?.size)
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseListOrNull(::TestEntity, src)?.size)
|
||||
assertEquals(2, parseListOrNull(src, ::TestEntity)?.size)
|
||||
|
||||
// error
|
||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseListOrNull(::TestEntity, src)?.size)
|
||||
assertEquals(2, parseListOrNull(src, ::TestEntity)?.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseMap() {
|
||||
assertEquals(0, parseMap(::TestEntity, null).size)
|
||||
assertEquals(0, parseMap(null, ::TestEntity).size)
|
||||
|
||||
val src = JsonArray()
|
||||
assertEquals(0, parseMap(::TestEntity, src).size)
|
||||
assertEquals(0, parseMap(null, ::TestEntity).size)
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(1, parseMap(::TestEntity, src).size)
|
||||
assertEquals(1, parseMap(src, ::TestEntity).size)
|
||||
|
||||
src.add("""{"s":"B","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseMap(::TestEntity, src).size)
|
||||
assertEquals(2, parseMap(src, ::TestEntity).size)
|
||||
|
||||
// error
|
||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseMap(::TestEntity, src).size)
|
||||
assertEquals(2, parseMap(src, ::TestEntity).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseMapOrNull() {
|
||||
assertEquals(null, parseMapOrNull(::TestEntity, null))
|
||||
assertEquals(null, parseMapOrNull(null, ::TestEntity))
|
||||
|
||||
val src = JsonArray()
|
||||
assertEquals(null, parseMapOrNull(::TestEntity, src))
|
||||
assertEquals(null, parseMapOrNull(src, ::TestEntity))
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(1, parseMapOrNull(::TestEntity, src)?.size)
|
||||
assertEquals(1, parseMapOrNull(src, ::TestEntity)?.size)
|
||||
|
||||
src.add("""{"s":"B","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseMapOrNull(::TestEntity, src)?.size)
|
||||
assertEquals(2, parseMapOrNull(src, ::TestEntity)?.size)
|
||||
|
||||
// error
|
||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseMapOrNull(::TestEntity, src)?.size)
|
||||
assertEquals(2, parseMapOrNull(src, ::TestEntity)?.size)
|
||||
}
|
||||
|
||||
private val parser by lazy {
|
||||
|
@ -154,42 +154,42 @@ class TestEntityUtils {
|
|||
@Test
|
||||
fun testParseItemWithParser() {
|
||||
|
||||
assertEquals(null, parseItem(::TestEntity, parser, null))
|
||||
assertEquals(null, parseItem(null) { TestEntity(parser, it) })
|
||||
|
||||
run {
|
||||
val src = """{"s":null,"l":"100"}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, parser, src)
|
||||
val item = parseItem(src) { TestEntity(parser, it) }
|
||||
assertNull(item)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"","l":"100"}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, parser, src)
|
||||
val item = parseItem(src) { TestEntity(parser, it) }
|
||||
assertNull(item)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"A","l":null}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, parser, src)
|
||||
val item = parseItem(src) { TestEntity(parser, it) }
|
||||
assertNotNull(item)
|
||||
assertEquals(src.optString("s"), item?.s)
|
||||
assertEquals(src.optLong("l"), item?.l)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"A","l":""}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, parser, src)
|
||||
val item = parseItem(src) { TestEntity(parser, it) }
|
||||
assertNotNull(item)
|
||||
assertEquals(src.optString("s"), item?.s)
|
||||
assertEquals(src.optLong("l"), item?.l)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"A","l":100}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, parser, src)
|
||||
val item = parseItem(src) { TestEntity(parser, it) }
|
||||
assertNotNull(item)
|
||||
assertEquals(src.optString("s"), item?.s)
|
||||
assertEquals(src.optLong("l"), item?.l)
|
||||
}
|
||||
run {
|
||||
val src = """{"s":"A","l":"100"}""".decodeJsonObject()
|
||||
val item = parseItem(::TestEntity, parser, src)
|
||||
val item = parseItem(src) { TestEntity(parser, it) }
|
||||
assertNotNull(item)
|
||||
assertEquals(src.optString("s"), item?.s)
|
||||
assertEquals(src.optLong("l"), item?.l)
|
||||
|
@ -198,38 +198,38 @@ class TestEntityUtils {
|
|||
|
||||
@Test
|
||||
fun testParseListWithParser() {
|
||||
assertEquals(0, parseList(::TestEntity, parser, null).size)
|
||||
assertEquals(0, parseList(null) { TestEntity(parser, it) }.size)
|
||||
|
||||
val src = JsonArray()
|
||||
assertEquals(0, parseList(::TestEntity, parser, src).size)
|
||||
assertEquals(0, parseList(src) { TestEntity(parser, it) }.size)
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(1, parseList(::TestEntity, parser, src).size)
|
||||
assertEquals(1, parseList(src) { TestEntity(parser, it) }.size)
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseList(::TestEntity, parser, src).size)
|
||||
assertEquals(2, parseList(src) { TestEntity(parser, it) }.size)
|
||||
|
||||
// error
|
||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseList(::TestEntity, parser, src).size)
|
||||
assertEquals(2, parseList(src) { TestEntity(parser, it) }.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseListOrNullWithParser() {
|
||||
assertEquals(null, parseListOrNull(::TestEntity, parser, null))
|
||||
assertEquals(null, parseListOrNull(null) { TestEntity(parser, it) })
|
||||
|
||||
val src = JsonArray()
|
||||
assertEquals(null, parseListOrNull(::TestEntity, parser, src))
|
||||
assertEquals(null, parseListOrNull(src) { TestEntity(parser, it) })
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(1, parseListOrNull(::TestEntity, parser, src)?.size)
|
||||
assertEquals(1, parseListOrNull(src) { TestEntity(parser, it) }?.size)
|
||||
|
||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size)
|
||||
assertEquals(2, parseListOrNull(src) { TestEntity(parser, it) }?.size)
|
||||
|
||||
// error
|
||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||
assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size)
|
||||
assertEquals(2, parseListOrNull(src) { TestEntity(parser, it) }?.size)
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException::class)
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.ContentValues
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
|
@ -16,12 +17,16 @@ import android.view.View
|
|||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.jrummyapps.android.colorpicker.ColorPickerDialog
|
||||
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.auth.AuthBase
|
||||
import jp.juggler.subwaytooter.api.auth.authRepo
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
|
||||
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||
import jp.juggler.subwaytooter.notification.*
|
||||
import jp.juggler.subwaytooter.push.PushBase
|
||||
|
@ -64,6 +69,7 @@ import kotlin.math.max
|
|||
|
||||
class ActAccountSetting : AppCompatActivity(),
|
||||
View.OnClickListener,
|
||||
ColorPickerDialogListener,
|
||||
CompoundButton.OnCheckedChangeListener,
|
||||
AdapterView.OnItemSelectedListener {
|
||||
companion object {
|
||||
|
@ -84,6 +90,8 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
|
||||
private const val ACTIVITY_STATE = "MyActivityState"
|
||||
|
||||
private const val COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR = 1
|
||||
|
||||
fun createIntent(activity: Activity, ai: SavedAccount) =
|
||||
Intent(activity, ActAccountSetting::class.java).apply {
|
||||
putExtra(KEY_ACCOUNT_DB_ID, ai.db_id)
|
||||
|
@ -510,6 +518,8 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
spResizeImage,
|
||||
swNotificationPullEnabled,
|
||||
swNotificationPushEnabled,
|
||||
btnNotificationAccentColorEdit,
|
||||
btnNotificationAccentColorReset,
|
||||
).forEach { it.isEnabledAlpha = enabled }
|
||||
|
||||
// arrayOf(
|
||||
|
@ -521,6 +531,7 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
showVisibility()
|
||||
showAcctColor()
|
||||
showPushSetting()
|
||||
showNotificationColor()
|
||||
} finally {
|
||||
loadingBusy = false
|
||||
}
|
||||
|
@ -547,6 +558,8 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
tvPushActions.vg(usePush)
|
||||
btnPushSubscription.vg(usePush)
|
||||
btnPushSubscriptionNotForce.vg(usePush)
|
||||
tvNotificationAccentColor.vg(usePush)
|
||||
llNotificationAccentColor.vg(usePush)
|
||||
}
|
||||
|
||||
run {
|
||||
|
@ -688,6 +701,22 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
// PullNotification.openNotificationChannelSetting(
|
||||
// this
|
||||
// )
|
||||
|
||||
R.id.btnNotificationAccentColorEdit -> {
|
||||
ColorPickerDialog.newBuilder().apply {
|
||||
setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
||||
setAllowPresets(true)
|
||||
setShowAlphaSlider(false)
|
||||
setDialogId(COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR)
|
||||
account.notificationAccentColor.notZero()?.let { setColor(it) }
|
||||
}.show(this)
|
||||
}
|
||||
|
||||
R.id.btnNotificationAccentColorReset -> {
|
||||
account.notificationAccentColor = 0
|
||||
saveUIToData()
|
||||
showNotificationColor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -788,16 +817,11 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
///////////////////////////////////////////////////
|
||||
|
||||
private fun performAccountRemove() {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.confirm)
|
||||
.setMessage(R.string.confirm_account_remove)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
launchAndShowError {
|
||||
authRepo.accountRemove(account)
|
||||
finish()
|
||||
}
|
||||
}.show()
|
||||
launchAndShowError {
|
||||
confirm(getString(R.string.confirm_account_remove), title = getString(R.string.confirm))
|
||||
authRepo.accountRemove(account)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
|
@ -1525,4 +1549,24 @@ class ActAccountSetting : AppCompatActivity(),
|
|||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDialogDismissed(dialogId: Int) {
|
||||
}
|
||||
|
||||
override fun onColorSelected(dialogId: Int, newColor: Int) {
|
||||
when (dialogId) {
|
||||
COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR -> {
|
||||
account.notificationAccentColor = newColor or Color.BLACK
|
||||
showNotificationColor()
|
||||
saveUIToData()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNotificationColor() {
|
||||
views.vNotificationAccentColorColor.backgroundColor =
|
||||
account.notificationAccentColor.notZero()
|
||||
?: ContextCompat.getColor(this, R.color.colorOsNotificationAccent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import jp.juggler.subwaytooter.api.dialogOrToast
|
||||
import jp.juggler.subwaytooter.api.entity.Acct
|
||||
import jp.juggler.subwaytooter.databinding.ActPushMessageListBinding
|
||||
import jp.juggler.subwaytooter.databinding.LvPushMessageBinding
|
||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||
|
@ -26,12 +25,14 @@ import jp.juggler.subwaytooter.push.pushRepo
|
|||
import jp.juggler.subwaytooter.table.PushMessage
|
||||
import jp.juggler.subwaytooter.table.daoAccountNotificationStatus
|
||||
import jp.juggler.subwaytooter.table.daoPushMessage
|
||||
import jp.juggler.subwaytooter.table.daoSavedAccount
|
||||
import jp.juggler.subwaytooter.util.permissionSpecNotification
|
||||
import jp.juggler.subwaytooter.util.requester
|
||||
import jp.juggler.util.coroutine.AppDispatchers
|
||||
import jp.juggler.util.coroutine.launchAndShowError
|
||||
import jp.juggler.util.data.encodeBase64Url
|
||||
import jp.juggler.util.data.notBlank
|
||||
import jp.juggler.util.data.notZero
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import jp.juggler.util.os.saveToDownload
|
||||
import jp.juggler.util.time.formatLocalTime
|
||||
|
@ -59,6 +60,9 @@ class ActPushMessageList : AppCompatActivity() {
|
|||
private val prNotification = permissionSpecNotification.requester {
|
||||
// 特に何もしない
|
||||
}
|
||||
private val acctMap by lazy {
|
||||
daoSavedAccount.loadRealAccounts().associateBy { it.acct }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
prNotification.register(this)
|
||||
|
@ -96,7 +100,7 @@ class ActPushMessageList : AppCompatActivity() {
|
|||
launchAndShowError {
|
||||
actionsDialog {
|
||||
action(getString(R.string.push_message_re_decode)) {
|
||||
pushRepo.reDecode(pm)
|
||||
pushRepo.reprocess(pm)
|
||||
}
|
||||
action(getString(R.string.push_message_save_to_download_folder)) {
|
||||
export(pm)
|
||||
|
@ -140,7 +144,7 @@ class ActPushMessageList : AppCompatActivity() {
|
|||
if (acct == null) {
|
||||
println("!!secret key is not exported because missing recepients acct.")
|
||||
} else {
|
||||
val status = daoAccountNotificationStatus.load(Acct.parse(acct))
|
||||
val status = daoAccountNotificationStatus.load(acct)
|
||||
if (status == null) {
|
||||
println("!!secret key is not exported because missing status for acct $acct .")
|
||||
} else {
|
||||
|
@ -157,12 +161,16 @@ class ActPushMessageList : AppCompatActivity() {
|
|||
|
||||
private val tintIconMap = HashMap<String, Drawable>()
|
||||
|
||||
fun tintIcon(ic: PushMessageIconColor) =
|
||||
tintIconMap.getOrPut(ic.name) {
|
||||
fun tintIcon(pm: PushMessage, ic: PushMessageIconColor) =
|
||||
tintIconMap.getOrPut("${ic.name}-${pm.loginAcct}") {
|
||||
val context = this
|
||||
val src = ContextCompat.getDrawable(context, ic.iconId)!!
|
||||
DrawableCompat.wrap(src).also {
|
||||
DrawableCompat.setTint(it, ContextCompat.getColor(context, ic.colorRes))
|
||||
DrawableCompat.wrap(src).also { d ->
|
||||
val a = acctMap[pm.loginAcct]
|
||||
val c = ic.colorRes.notZero()?.let { ContextCompat.getColor(context, it) }
|
||||
?: a?.notificationAccentColor?.notZero()
|
||||
?: ContextCompat.getColor(this, R.color.colorOsNotificationAccent)
|
||||
DrawableCompat.setTint(d, c)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,10 +191,9 @@ class ActPushMessageList : AppCompatActivity() {
|
|||
pm ?: return
|
||||
lastItem = pm
|
||||
|
||||
val iconAndColor = pm.iconColor()
|
||||
Glide.with(views.ivSmall)
|
||||
.load(pm.iconSmall)
|
||||
.error(tintIcon(iconAndColor))
|
||||
.error(tintIcon(pm, pm.iconColor()))
|
||||
.into(views.ivSmall)
|
||||
|
||||
Glide.with(views.ivLarge)
|
||||
|
|
|
@ -1,22 +1,29 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.PictureDrawable
|
||||
import androidx.annotation.Nullable
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.integration.webp.decoder.*
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.ResourceDecoder
|
||||
import com.bumptech.glide.load.engine.Resource
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
|
||||
import com.bumptech.glide.load.resource.SimpleResource
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.caverock.androidsvg.SVG
|
||||
import com.caverock.androidsvg.SVGParseException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.min
|
||||
|
||||
@GlideModule
|
||||
|
@ -73,7 +80,6 @@ class MyAppGlideModule : AppGlideModule() {
|
|||
// Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]).
|
||||
class SvgDrawableTranscoder : ResourceTranscoder<SVG, PictureDrawable> {
|
||||
|
||||
@Nullable
|
||||
override fun transcode(
|
||||
toTranscode: Resource<SVG>,
|
||||
options: Options,
|
||||
|
@ -102,6 +108,65 @@ class MyAppGlideModule : AppGlideModule() {
|
|||
registry
|
||||
.register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder())
|
||||
.append(InputStream::class.java, SVG::class.java, SvgDecoder())
|
||||
|
||||
///////
|
||||
// Animated WebP
|
||||
|
||||
// // We should put our decoder before the build-in decoders,
|
||||
// // because the Downsampler will consume arbitrary data and make the inputstream corrupt
|
||||
// // on some devices
|
||||
// val resources: Resources = context.resources
|
||||
// val bitmapPool: BitmapPool = glide.bitmapPool
|
||||
// val arrayPool: ArrayPool = glide.arrayPool
|
||||
// /* static webp decoders */
|
||||
// val webpDownsampler = WebpDownsampler(
|
||||
// registry.imageHeaderParsers,
|
||||
// resources.getDisplayMetrics(), bitmapPool, arrayPool
|
||||
// )
|
||||
// val bitmapDecoder = AnimatedWebpBitmapDecoder(arrayPool, bitmapPool)
|
||||
// val byteBufferBitmapDecoder = ByteBufferBitmapWebpDecoder(webpDownsampler)
|
||||
// val streamBitmapDecoder = StreamBitmapWebpDecoder(webpDownsampler, arrayPool)
|
||||
// /* animate webp decoders */
|
||||
// val byteBufferWebpDecoder = ByteBufferWebpDecoder(context, arrayPool, bitmapPool)
|
||||
// registry /* Bitmaps for static webp images */
|
||||
// .prepend(
|
||||
// Registry.BUCKET_BITMAP,
|
||||
// ByteBuffer::class.java,
|
||||
// Bitmap::class.java, byteBufferBitmapDecoder
|
||||
// )
|
||||
// .prepend(
|
||||
// Registry.BUCKET_BITMAP,
|
||||
// InputStream::class.java,
|
||||
// Bitmap::class.java, streamBitmapDecoder
|
||||
// ) /* BitmapDrawables for static webp images */
|
||||
// .prepend(
|
||||
// Registry.BUCKET_BITMAP_DRAWABLE,
|
||||
// ByteBuffer::class.java,
|
||||
// BitmapDrawable::class.java,
|
||||
// BitmapDrawableDecoder(resources, byteBufferBitmapDecoder)
|
||||
// )
|
||||
// .prepend(
|
||||
// Registry.BUCKET_BITMAP_DRAWABLE,
|
||||
// InputStream::class.java,
|
||||
// BitmapDrawable::class.java,
|
||||
// BitmapDrawableDecoder(resources, streamBitmapDecoder)
|
||||
// ) /* Bitmaps for animated webp images*/
|
||||
// .prepend(
|
||||
// Registry.BUCKET_BITMAP,
|
||||
// ByteBuffer::class.java,
|
||||
// Bitmap::class.java, ByteBufferAnimatedBitmapDecoder(bitmapDecoder)
|
||||
// )
|
||||
// .prepend(
|
||||
// Registry.BUCKET_BITMAP,
|
||||
// InputStream::class.java,
|
||||
// Bitmap::class.java, StreamAnimatedBitmapDecoder(bitmapDecoder)
|
||||
// ) /* Animated webp images */
|
||||
// .prepend(ByteBuffer::class.java, WebpDrawable::class.java, byteBufferWebpDecoder)
|
||||
// .prepend(
|
||||
// InputStream::class.java,
|
||||
// WebpDrawable::class.java, StreamWebpDecoder(byteBufferWebpDecoder, arrayPool)
|
||||
// )
|
||||
// .prepend(WebpDrawable::class.java, WebpDrawableEncoder())
|
||||
}
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
|
|
|
@ -70,7 +70,7 @@ fun ActMain.accountAdd() {
|
|||
val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) }
|
||||
addPseudoAccount(apiHost, tootInstance)?.let { a ->
|
||||
showToast(false, R.string.server_confirmed)
|
||||
addColumn(defaultInsertPosition, a, ColumnType.LOCAL)
|
||||
addColumn(defaultInsertPosition, a, ColumnType.LOCAL,protect=true)
|
||||
dialogHost.dismissSafe()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -383,7 +383,7 @@ fun ActMain.clickBoostBy(
|
|||
columnType: ColumnType = ColumnType.BOOSTED_BY,
|
||||
) {
|
||||
status ?: return
|
||||
addColumn(false, pos, accessInfo, columnType, status.id)
|
||||
addColumn(false, pos, accessInfo, columnType, params= arrayOf(status.id))
|
||||
}
|
||||
|
||||
fun ActMain.clickBoost(accessInfo: SavedAccount, status: TootStatus, willToast: Boolean) {
|
||||
|
|
|
@ -164,7 +164,7 @@ fun ActMain.conversationLocal(
|
|||
else ->
|
||||
ColumnType.CONVERSATION
|
||||
},
|
||||
statusId,
|
||||
params = arrayOf(statusId),
|
||||
)
|
||||
|
||||
private val reDetailedStatusTime =
|
||||
|
@ -501,7 +501,10 @@ fun ActMain.conversationFromTootsearch(
|
|||
val replyId = status?.in_reply_to_id
|
||||
when {
|
||||
status == null -> showToast(true, result.error ?: "?")
|
||||
replyId == null -> showToast(true, "showReplyTootsearch: in_reply_to_id is null")
|
||||
replyId == null -> showToast(
|
||||
true,
|
||||
"showReplyTootsearch: in_reply_to_id is null"
|
||||
)
|
||||
else -> conversationLocal(pos, a, replyId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,10 @@ import okhttp3.Request
|
|||
|
||||
fun ActMain.clickListTl(pos: Int, accessInfo: SavedAccount, item: TimelineItem?) {
|
||||
when (item) {
|
||||
is TootList -> addColumn(pos, accessInfo, ColumnType.LIST_TL, item.id)
|
||||
is MisskeyAntenna -> addColumn(pos, accessInfo, ColumnType.MISSKEY_ANTENNA_TL, item.id)
|
||||
is TootList ->
|
||||
addColumn(pos, accessInfo, ColumnType.LIST_TL, params = arrayOf(item.id))
|
||||
is MisskeyAntenna ->
|
||||
addColumn(pos, accessInfo, ColumnType.MISSKEY_ANTENNA_TL, params = arrayOf(item.id))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,7 +41,7 @@ fun ActMain.clickListMoreButton(pos: Int, accessInfo: SavedAccount, item: Timeli
|
|||
launchAndShowError {
|
||||
actionsDialog(item.title) {
|
||||
action(getString(R.string.list_timeline)) {
|
||||
addColumn(pos, accessInfo, ColumnType.LIST_TL, item.id)
|
||||
addColumn(pos, accessInfo, ColumnType.LIST_TL, params = arrayOf(item.id))
|
||||
}
|
||||
action(getString(R.string.list_member)) {
|
||||
addColumn(
|
||||
|
@ -47,7 +49,7 @@ fun ActMain.clickListMoreButton(pos: Int, accessInfo: SavedAccount, item: Timeli
|
|||
pos,
|
||||
accessInfo,
|
||||
ColumnType.LIST_MEMBER,
|
||||
item.id
|
||||
params = arrayOf(item.id)
|
||||
)
|
||||
}
|
||||
action(getString(R.string.rename)) {
|
||||
|
|
|
@ -26,12 +26,7 @@ fun ActMain.clickNotificationFrom(
|
|||
showToast(false, R.string.misskey_account_not_supported)
|
||||
} else {
|
||||
accessInfo.getFullAcct(who).validFull()?.let {
|
||||
addColumn(
|
||||
pos,
|
||||
accessInfo,
|
||||
ColumnType.NOTIFICATION_FROM_ACCT,
|
||||
it
|
||||
)
|
||||
addColumn(pos, accessInfo, ColumnType.NOTIFICATION_FROM_ACCT, params = arrayOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ private fun ActMain.serverProfileDirectory(
|
|||
pos,
|
||||
accessInfo,
|
||||
ColumnType.PROFILE_DIRECTORY,
|
||||
host
|
||||
params = arrayOf(host)
|
||||
)
|
||||
|
||||
// 疑似アカウントで開く
|
||||
|
@ -70,7 +70,7 @@ private fun ActMain.serverProfileDirectory(
|
|||
pos,
|
||||
ai,
|
||||
ColumnType.PROFILE_DIRECTORY,
|
||||
host
|
||||
params = arrayOf(host)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +116,7 @@ fun ActMain.serverInformation(
|
|||
pos,
|
||||
SavedAccount.na,
|
||||
ColumnType.INSTANCE_INFORMATION,
|
||||
host
|
||||
params = arrayOf(host),
|
||||
)
|
||||
|
||||
// ドメインブロック一覧から解除
|
||||
|
|
|
@ -607,5 +607,5 @@ fun ActMain.openStatusHistory(
|
|||
accessInfo: SavedAccount,
|
||||
status: TootStatus,
|
||||
) {
|
||||
addColumn(pos, accessInfo, ColumnType.STATUS_HISTORY, status.id, status.json)
|
||||
addColumn(pos, accessInfo, ColumnType.STATUS_HISTORY, params = arrayOf(status.id, status.json))
|
||||
}
|
||||
|
|
|
@ -145,14 +145,13 @@ fun ActMain.tagTimeline(
|
|||
acctAscii: String? = null,
|
||||
) {
|
||||
if (acctAscii == null) {
|
||||
addColumn(pos, accessInfo, ColumnType.HASHTAG, tagWithoutSharp)
|
||||
addColumn(pos, accessInfo, ColumnType.HASHTAG, params = arrayOf(tagWithoutSharp))
|
||||
} else {
|
||||
addColumn(
|
||||
pos,
|
||||
accessInfo,
|
||||
ColumnType.HASHTAG_FROM_ACCT,
|
||||
tagWithoutSharp,
|
||||
acctAscii
|
||||
params = arrayOf(tagWithoutSharp, acctAscii)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,12 +38,14 @@ fun ActMain.timeline(
|
|||
)?.let { account ->
|
||||
when (type) {
|
||||
ColumnType.PROFILE ->
|
||||
account.loginAccount?.id?.let { addColumn(pos, account, type, it) }
|
||||
account.loginAccount?.id?.let {
|
||||
addColumn(pos, account, type, params = arrayOf(it))
|
||||
}
|
||||
|
||||
ColumnType.PROFILE_DIRECTORY ->
|
||||
addColumn(pos, account, type, account.apiHost)
|
||||
addColumn(pos, account, type, params = arrayOf(account.apiHost))
|
||||
|
||||
else -> addColumn(pos, account, type, *args)
|
||||
else -> addColumn(pos, account, type, params = args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +97,7 @@ fun ActMain.timelineDomain(
|
|||
pos: Int,
|
||||
accessInfo: SavedAccount,
|
||||
host: Host,
|
||||
) = addColumn(pos, accessInfo, ColumnType.DOMAIN_TIMELINE, host)
|
||||
) = addColumn(pos, accessInfo, ColumnType.DOMAIN_TIMELINE, params = arrayOf(host))
|
||||
|
||||
// 指定タンスのローカルタイムラインを開く
|
||||
fun ActMain.timelineLocal(
|
||||
|
@ -131,7 +133,7 @@ private fun ActMain.timelineAround(
|
|||
pos: Int,
|
||||
id: EntityId,
|
||||
type: ColumnType,
|
||||
) = addColumn(pos, accessInfo, type, id)
|
||||
) = addColumn(pos, accessInfo, type, params = arrayOf(id))
|
||||
|
||||
// 投稿を同期してstatusIdを調べてから指定アカウントでタイムラインを開く
|
||||
private fun ActMain.timelineAroundByStatus(
|
||||
|
|
|
@ -573,7 +573,7 @@ private fun ActMain.userProfileFromUrlOrAcct(
|
|||
openCustomTab(whoUrl)
|
||||
}
|
||||
|
||||
else -> addColumn(pos, accessInfo, ColumnType.PROFILE, who.id)
|
||||
else -> addColumn(pos, accessInfo, ColumnType.PROFILE, params = arrayOf(who.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -597,7 +597,7 @@ fun ActMain.userProfileFromAnotherAccount(
|
|||
accountListArg = accountListNonPseudo(who.apDomain)
|
||||
)?.let { ai ->
|
||||
if (ai.matchHost(accessInfo)) {
|
||||
addColumn(pos, ai, ColumnType.PROFILE, who.id)
|
||||
addColumn(pos, ai, ColumnType.PROFILE, params = arrayOf(who.id))
|
||||
} else {
|
||||
userProfileFromUrlOrAcct(pos, ai, accessInfo.getFullAcct(who), who.url)
|
||||
}
|
||||
|
@ -613,7 +613,7 @@ fun ActMain.userProfileLocal(
|
|||
) {
|
||||
when {
|
||||
accessInfo.isNA -> userProfileFromAnotherAccount(pos, accessInfo, who)
|
||||
else -> addColumn(pos, accessInfo, ColumnType.PROFILE, who.id)
|
||||
else -> addColumn(pos, accessInfo, ColumnType.PROFILE, params = arrayOf(who.id))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,8 @@ fun ActMain.addColumn(
|
|||
indexArg: Int,
|
||||
ai: SavedAccount,
|
||||
type: ColumnType,
|
||||
vararg params: Any,
|
||||
protect: Boolean = false,
|
||||
params: Array<out Any> = emptyArray(),
|
||||
): Column {
|
||||
if (!allowColumnDuplication) {
|
||||
// 既に同じカラムがあればそこに移動する
|
||||
|
@ -90,7 +91,8 @@ fun ActMain.addColumn(
|
|||
}
|
||||
|
||||
//
|
||||
val col = Column(appState, ai, type.id, *params)
|
||||
val col = Column(appState, ai, type.id, params)
|
||||
if (protect) col.dontClose = true
|
||||
val index = addColumn(col, indexArg)
|
||||
scrollAndLoad(index)
|
||||
return col
|
||||
|
@ -100,16 +102,16 @@ fun ActMain.addColumn(
|
|||
indexArg: Int,
|
||||
ai: SavedAccount,
|
||||
type: ColumnType,
|
||||
vararg params: Any,
|
||||
): Column {
|
||||
return addColumn(
|
||||
PrefB.bpAllowColumnDuplication.value,
|
||||
indexArg,
|
||||
ai,
|
||||
type,
|
||||
*params
|
||||
)
|
||||
}
|
||||
protect: Boolean = false,
|
||||
params: Array<out Any> = emptyArray(),
|
||||
): Column = addColumn(
|
||||
PrefB.bpAllowColumnDuplication.value,
|
||||
indexArg,
|
||||
ai,
|
||||
type,
|
||||
protect = protect,
|
||||
params = params,
|
||||
)
|
||||
|
||||
fun ActMain.removeColumn(column: Column) {
|
||||
val idxColumn = appState.columnIndex(column) ?: return
|
||||
|
@ -342,7 +344,7 @@ fun ActMain.searchFromActivityResult(data: Intent?, columnType: ColumnType) =
|
|||
defaultInsertPosition,
|
||||
SavedAccount.na,
|
||||
columnType,
|
||||
it
|
||||
params = arrayOf(it)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -320,11 +320,11 @@ private fun ActMain.afterAccountAdd(
|
|||
}
|
||||
|
||||
// 適当にカラムを追加する
|
||||
addColumn(false, defaultInsertPosition, account, ColumnType.HOME)
|
||||
addColumn(false, defaultInsertPosition, account, ColumnType.HOME, protect = true)
|
||||
if (daoSavedAccount.isSingleAccount()) {
|
||||
addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS)
|
||||
addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL)
|
||||
addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE)
|
||||
addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS, protect = true)
|
||||
addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL, protect = true)
|
||||
addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE, protect = true)
|
||||
}
|
||||
|
||||
// 通知の更新が必要かもしれない
|
||||
|
|
|
@ -26,7 +26,10 @@ import jp.juggler.subwaytooter.api.entity.TootStatus
|
|||
import jp.juggler.subwaytooter.column.ColumnType
|
||||
import jp.juggler.subwaytooter.dialog.pickAccount
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
import jp.juggler.subwaytooter.pref.PrefDevice.Companion.PUSH_DISTRIBUTOR_NONE
|
||||
import jp.juggler.subwaytooter.pref.PrefS
|
||||
import jp.juggler.subwaytooter.pref.prefDevice
|
||||
import jp.juggler.subwaytooter.push.fcmHandler
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.table.accountListCanSeeMyReactions
|
||||
import jp.juggler.subwaytooter.util.VersionString
|
||||
|
@ -404,7 +407,12 @@ class SideMenuAdapter(
|
|||
// },
|
||||
|
||||
Item(icon = R.drawable.ic_search, title = R.string.notestock) {
|
||||
addColumn(defaultInsertPosition, SavedAccount.na, ColumnType.SEARCH_NOTESTOCK, "")
|
||||
addColumn(
|
||||
defaultInsertPosition,
|
||||
SavedAccount.na,
|
||||
ColumnType.SEARCH_NOTESTOCK,
|
||||
params = arrayOf("")
|
||||
)
|
||||
},
|
||||
|
||||
Item(),
|
||||
|
@ -524,7 +532,8 @@ class SideMenuAdapter(
|
|||
ItemType.IT_NOTIFICATION_PERMISSION ->
|
||||
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
|
||||
isAllCaps = false
|
||||
text = actMain.getString(R.string.notification_permission_not_granted)
|
||||
val action = notificationActionRecommend() ?: return@apply
|
||||
text = actMain.getString(action.first)
|
||||
val drawable = createColoredDrawable(actMain, icon, iconColor, 1f)
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
drawable,
|
||||
|
@ -534,11 +543,8 @@ class SideMenuAdapter(
|
|||
)
|
||||
setOnClickListener {
|
||||
drawer.closeDrawer(GravityCompat.START)
|
||||
if (actMain.prNotification.spec.listNotGranded(actMain).isNotEmpty()) {
|
||||
actMain.prNotification.openAppSetting(actMain)
|
||||
} else {
|
||||
filterListItems()
|
||||
}
|
||||
notificationActionRecommend()?.second?.invoke()
|
||||
filterListItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -581,11 +587,24 @@ class SideMenuAdapter(
|
|||
this.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun notificationActionRecommend(): Pair<Int, () -> Unit>? = when {
|
||||
actMain.prNotification.spec.listNotGranded(actMain).isNotEmpty() ->
|
||||
Pair(R.string.notification_permission_not_granted) {
|
||||
actMain.prNotification.openAppSetting(actMain)
|
||||
}
|
||||
(actMain.prefDevice.pushDistributor.isNullOrEmpty() && actMain.fcmHandler.noFcm) ||
|
||||
actMain.prefDevice.pushDistributor == PUSH_DISTRIBUTOR_NONE ->
|
||||
Pair(R.string.notification_push_distributor_disabled) {
|
||||
actMain.selectPushDistributor()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun filterListItems() {
|
||||
list = originalList.filter {
|
||||
when (it.itemType) {
|
||||
ItemType.IT_NOTIFICATION_PERMISSION ->
|
||||
actMain.prNotification.spec.listNotGranded(actMain).isNotEmpty()
|
||||
notificationActionRecommend() != null
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import jp.juggler.subwaytooter.App1
|
|||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.auth.AuthBase
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
import jp.juggler.util.data.*
|
||||
|
@ -603,7 +604,7 @@ suspend fun TootApiClient.syncAccountByUrl(
|
|||
}.toPostRequestBuilder()
|
||||
)
|
||||
?.apply {
|
||||
ar = TootAccountRef.mayNull(parser, parser.account(jsonObject))
|
||||
ar = tootAccountRefOrNull(parser, parser.account(jsonObject))
|
||||
if (ar == null && error == null) {
|
||||
setError(context.getString(R.string.user_id_conversion_failed))
|
||||
}
|
||||
|
@ -646,7 +647,7 @@ suspend fun TootApiClient.syncAccountByAcct(
|
|||
.toPostRequestBuilder()
|
||||
)
|
||||
?.apply {
|
||||
ar = TootAccountRef.mayNull(parser, parser.account(jsonObject))
|
||||
ar = tootAccountRefOrNull(parser, parser.account(jsonObject))
|
||||
if (ar == null && error == null) {
|
||||
setError(context.getString(R.string.user_id_conversion_failed))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package jp.juggler.subwaytooter.api.auth
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
|
@ -8,24 +9,26 @@ import jp.juggler.subwaytooter.notification.checkNotificationImmediate
|
|||
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
|
||||
import jp.juggler.subwaytooter.pref.PrefL
|
||||
import jp.juggler.subwaytooter.pref.lazyContext
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.table.appDatabase
|
||||
import jp.juggler.subwaytooter.table.*
|
||||
import jp.juggler.util.log.LogCategory
|
||||
|
||||
val Context.authRepo
|
||||
get() = AuthRepo(
|
||||
context = this,
|
||||
daoAcctColor = AcctColor.Access(appDatabase),
|
||||
daoSavedAccount = SavedAccount.Access(appDatabase, lazyContext),
|
||||
database = appDatabase,
|
||||
)
|
||||
|
||||
class AuthRepo(
|
||||
private val context: Context = lazyContext,
|
||||
private val database: SQLiteDatabase = appDatabase,
|
||||
private val daoAcctColor: AcctColor.Access =
|
||||
AcctColor.Access(appDatabase),
|
||||
AcctColor.Access(database),
|
||||
private val daoSavedAccount: SavedAccount.Access =
|
||||
SavedAccount.Access(appDatabase, lazyContext),
|
||||
SavedAccount.Access(database, context),
|
||||
private val daoPushMessage: PushMessage.Access =
|
||||
PushMessage.Access(database),
|
||||
private val daoNotificationShown: NotificationShown.Access =
|
||||
NotificationShown.Access(database),
|
||||
) {
|
||||
companion object {
|
||||
private val log = LogCategory("AuthRepo")
|
||||
|
@ -69,6 +72,8 @@ class AuthRepo(
|
|||
PrefL.lpTabletTootDefaultAccount.value = -1L
|
||||
}
|
||||
daoSavedAccount.delete(account.db_id)
|
||||
daoPushMessage.deleteAccount(account.acct)
|
||||
daoNotificationShown.cleayByAcct(account.acct)
|
||||
// appServerUnregister(context.applicationContextSafe, account)
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ class APTag(parser: TootParser, jsonArray: JsonArray?) {
|
|||
)
|
||||
} else {
|
||||
emojiList[shortcode] = CustomEmoji(
|
||||
apDomain = parser.apDomain,
|
||||
shortcode = shortcode,
|
||||
url = iconUrl,
|
||||
staticUrl = iconUrl,
|
||||
|
|
|
@ -4,11 +4,15 @@ import jp.juggler.subwaytooter.emoji.CustomEmoji
|
|||
import jp.juggler.util.data.JsonObject
|
||||
import jp.juggler.util.log.LogCategory
|
||||
|
||||
class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) {
|
||||
companion object {
|
||||
private val log = LogCategory("MisskeyNoteUpdate")
|
||||
}
|
||||
|
||||
class MisskeyNoteUpdate(
|
||||
val noteId: EntityId,
|
||||
val type: Type,
|
||||
var reaction: String? = null,
|
||||
var userId: EntityId? = null,
|
||||
var deletedAt: Long? = null,
|
||||
var choice: Int? = null,
|
||||
var emoji: CustomEmoji? = null,
|
||||
) {
|
||||
enum class Type {
|
||||
REACTION,
|
||||
UNREACTION,
|
||||
|
@ -16,53 +20,34 @@ class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) {
|
|||
VOTED
|
||||
}
|
||||
|
||||
val noteId: EntityId
|
||||
val type: Type
|
||||
var reaction: String? = null
|
||||
var userId: EntityId? = null
|
||||
var deletedAt: Long? = null
|
||||
var choice: Int? = null
|
||||
var emoji: CustomEmoji? = null
|
||||
companion object {
|
||||
|
||||
init {
|
||||
noteId = EntityId.mayNull(src.string("id")) ?: error("MisskeyNoteUpdate: missing note id")
|
||||
fun misskeyNoteUpdate(src: JsonObject): MisskeyNoteUpdate {
|
||||
|
||||
// root.body.body
|
||||
val body2 = src.jsonObject("body") ?: error("MisskeyNoteUpdate: missing body")
|
||||
val noteId = EntityId.mayNull(src.string("id"))
|
||||
?: error("MisskeyNoteUpdate: missing note id")
|
||||
|
||||
when (val strType = src.string("type")) {
|
||||
"reacted" -> {
|
||||
type = Type.REACTION
|
||||
reaction = body2.string("reaction")
|
||||
userId = EntityId.mayDefault(body2.string("userId"))
|
||||
emoji = body2.jsonObject("emoji")?.let {
|
||||
try {
|
||||
CustomEmoji.decodeMisskey(apDomain, apiHost, it)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "can't parse custom emoji.")
|
||||
null
|
||||
}
|
||||
}
|
||||
// root.body.body
|
||||
val body2 = src.jsonObject("body")
|
||||
?: error("MisskeyNoteUpdate: missing body")
|
||||
|
||||
val type: Type = when (val strType = src.string("type")) {
|
||||
"reacted" -> Type.REACTION
|
||||
"unreacted" -> Type.UNREACTION
|
||||
"deleted" -> Type.DELETED
|
||||
"pollVoted" -> Type.VOTED
|
||||
else -> error("MisskeyNoteUpdate: unknown type $strType")
|
||||
}
|
||||
|
||||
"unreacted" -> {
|
||||
type = Type.UNREACTION
|
||||
reaction = body2.string("reaction")
|
||||
userId = EntityId.mayDefault(body2.string("userId"))
|
||||
}
|
||||
|
||||
"deleted" -> {
|
||||
type = Type.DELETED
|
||||
deletedAt = TootStatus.parseTime(body2.string("deletedAt"))
|
||||
}
|
||||
|
||||
"pollVoted" -> {
|
||||
type = Type.VOTED
|
||||
choice = body2.int("choice")
|
||||
userId = EntityId.mayDefault(body2.string("userId"))
|
||||
}
|
||||
|
||||
else -> error("MisskeyNoteUpdate: unknown type $strType")
|
||||
return MisskeyNoteUpdate(
|
||||
noteId = noteId,
|
||||
type = type,
|
||||
reaction = body2.string("reaction"),
|
||||
userId = EntityId.mayNull(body2.string("userId")),
|
||||
deletedAt = body2.string("deletedAt")?.let { TootStatus.parseTime(it) },
|
||||
choice = body2.int("choice"),
|
||||
emoji = parseItem(body2.jsonObject("emoji"), CustomEmoji::decodeMisskey),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.widget.TextView
|
|||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.MisskeyAccountDetailMap
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
|
@ -386,6 +387,8 @@ open class TootAccount(
|
|||
"""\Ahttps://($reHostIdn)/users/(\w|\w+[\w-]*\w)(?=\z|[?#])"""
|
||||
.asciiPattern()
|
||||
|
||||
private val reMisskeyIoProxy = """\Ahttps://misskey\.io/proxy/""".toRegex()
|
||||
|
||||
fun tootAccount(parser: TootParser, src: JsonObject): TootAccount {
|
||||
src["_fromStream"] = parser.fromStream
|
||||
|
||||
|
@ -427,9 +430,7 @@ open class TootAccount(
|
|||
ServiceType.MISSKEY -> {
|
||||
|
||||
custom_emojis =
|
||||
parseMapOrNull(src.jsonArray("emojis")) {
|
||||
CustomEmoji.decodeMisskey(parser.apDomain, parser.apiHost, it)
|
||||
}
|
||||
parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMisskey)
|
||||
profile_emojis = null
|
||||
|
||||
username = src.stringOrThrow("username")
|
||||
|
@ -476,17 +477,14 @@ open class TootAccount(
|
|||
created_at = src.string("createdAt")
|
||||
time_created_at = TootStatus.parseTime(created_at)
|
||||
|
||||
// https://github.com/syuilo/misskey/blob/develop/src/client/scripts/get-static-image-url.ts
|
||||
fun String.getStaticImageUrl(): String? {
|
||||
val uri = this.mayUri() ?: return null
|
||||
val dummy = "${uri.encodedAuthority}${uri.encodedPath}"
|
||||
return "https://${parser.linkHelper.apiHost.ascii}/proxy/$dummy?url=${encodePercent()}&static=1"
|
||||
}
|
||||
|
||||
// 画像を静止させるURLはAPIとしては提供されていない
|
||||
// サーバ側で実装されている方法は仕様が安定しない
|
||||
// クライアント側でアニメーションを止めるのが正解らしいが、
|
||||
// 対応できてないな…
|
||||
avatar = src.string("avatarUrl")
|
||||
avatar_static = src.string("avatarUrl")?.getStaticImageUrl()
|
||||
avatar_static = src.string("avatarUrl")
|
||||
header = src.string("bannerUrl")
|
||||
header_static = src.string("bannerUrl")?.getStaticImageUrl()
|
||||
header_static = src.string("bannerUrl")
|
||||
|
||||
pinnedNoteIds = src.stringArrayList("pinnedNoteIds")
|
||||
if (parser.misskeyDecodeProfilePin) {
|
||||
|
@ -561,13 +559,8 @@ open class TootAccount(
|
|||
else -> {
|
||||
|
||||
// 絵文字データは先に読んでおく
|
||||
custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
|
||||
CustomEmoji.decode(
|
||||
parser.apDomain,
|
||||
parser.apiHost,
|
||||
it
|
||||
)
|
||||
}
|
||||
custom_emojis =
|
||||
parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMastodon)
|
||||
|
||||
profile_emojis = when (val o = src["profile_emojis"]) {
|
||||
is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) }
|
||||
|
@ -587,7 +580,7 @@ open class TootAccount(
|
|||
note = src.string("note")
|
||||
|
||||
source = parseSource(src.jsonObject("source"))
|
||||
movedRef = TootAccountRef.mayNull(
|
||||
movedRef = tootAccountRefOrNull(
|
||||
parser,
|
||||
src.jsonObject("moved")?.let {
|
||||
tootAccount(parser, it)
|
||||
|
@ -692,8 +685,8 @@ open class TootAccount(
|
|||
acct = acct,
|
||||
apDomain = apDomain,
|
||||
apiHost = apiHost,
|
||||
avatar = avatar,
|
||||
avatar_static = avatar_static,
|
||||
avatar = avatar?.replace(reMisskeyIoProxy, "https://"),
|
||||
avatar_static = avatar_static?.replace(reMisskeyIoProxy, "https://"),
|
||||
birthday = birthday,
|
||||
bot = bot,
|
||||
created_at = created_at,
|
||||
|
|
|
@ -18,10 +18,7 @@ class TootAccountRef private constructor(
|
|||
fun get() = TootAccountMap.find(this)
|
||||
|
||||
companion object {
|
||||
fun notNull(parser: TootParser, account: TootAccount) =
|
||||
tootAccountRef(parser, account)
|
||||
|
||||
fun mayNull(parser: TootParser, account: TootAccount?): TootAccountRef? {
|
||||
fun tootAccountRefOrNull(parser: TootParser, account: TootAccount?): TootAccountRef? {
|
||||
return when (account) {
|
||||
null -> null
|
||||
else -> tootAccountRef(parser, account)
|
||||
|
|
|
@ -38,9 +38,7 @@ class TootAnnouncement(
|
|||
private val log = LogCategory("TootAnnouncement")
|
||||
|
||||
fun tootAnnouncement(parser: TootParser, src: JsonObject): TootAnnouncement {
|
||||
val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
|
||||
CustomEmoji.decode(parser.apDomain, parser.apiHost, it)
|
||||
}
|
||||
val custom_emojis = parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMastodon)
|
||||
val reactions = parseListOrNull(src.jsonArray("reactions")) {
|
||||
TootReaction.parseFedibird(it)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.api.entity
|
|||
import android.content.Context
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
import jp.juggler.util.data.JsonObject
|
||||
import jp.juggler.util.data.notEmpty
|
||||
|
@ -81,90 +82,79 @@ class TootNotification(
|
|||
const val TYPE_STATUS_REFERENCE = "status_reference"
|
||||
const val TYPE_SCHEDULED_STATUS = "scheduled_status"
|
||||
|
||||
fun tootNotification(parser: TootParser, src: JsonObject): TootNotification {
|
||||
val id: EntityId
|
||||
// One of: "mention", "reblog", "favourite", "follow"
|
||||
val type: String
|
||||
// The Account sending the notification to the user
|
||||
val accountRef: TootAccountRef?
|
||||
private fun tootNotificationMisskey(parser: TootParser, src: JsonObject): TootNotification {
|
||||
// Misskeyの通知APIはページネーションをIDでしか行えない
|
||||
// これは改善される予定 https://github.com/syuilo/misskey/issues/2275
|
||||
|
||||
// The Status associated with the notification, if applicable
|
||||
// 投稿の更新により変更可能になる
|
||||
val status: TootStatus?
|
||||
val created_at: String? = src.string("createdAt")
|
||||
|
||||
val reaction: TootReaction?
|
||||
val accountRef = tootAccountRefOrNull(
|
||||
parser,
|
||||
parser.account(src.jsonObject("user"))
|
||||
)
|
||||
|
||||
val reblog_visibility: TootVisibility
|
||||
val reaction: TootReaction? = src.string("reaction")
|
||||
?.notEmpty()
|
||||
?.let { TootReaction.parseMisskey(it) }
|
||||
|
||||
// The time the notification was created
|
||||
val created_at: String?
|
||||
val time_created_at: Long
|
||||
|
||||
|
||||
if (parser.serviceType == ServiceType.MISSKEY) {
|
||||
id = EntityId.mayDefault(src.string("id"))
|
||||
|
||||
type = src.stringOrThrow("type")
|
||||
|
||||
created_at = src.string("createdAt")
|
||||
time_created_at = TootStatus.parseTime(created_at)
|
||||
|
||||
accountRef = TootAccountRef.mayNull(
|
||||
parser,
|
||||
parser.account(
|
||||
src.jsonObject("user")
|
||||
)
|
||||
)
|
||||
status = parser.status(
|
||||
src.jsonObject("note")
|
||||
)
|
||||
|
||||
reaction = src.string("reaction")
|
||||
?.notEmpty()
|
||||
?.let { TootReaction.parseMisskey(it) }
|
||||
|
||||
reblog_visibility = TootVisibility.Unknown
|
||||
|
||||
// Misskeyの通知APIはページネーションをIDでしか行えない
|
||||
// これは改善される予定 https://github.com/syuilo/misskey/issues/2275
|
||||
} else {
|
||||
id = EntityId.mayDefault(src.string("id"))
|
||||
|
||||
type = src.stringOrThrow("type")
|
||||
|
||||
created_at = src.string("created_at")
|
||||
time_created_at = TootStatus.parseTime(created_at)
|
||||
accountRef =
|
||||
TootAccountRef.mayNull(parser, parser.account(src.jsonObject("account")))
|
||||
status = parser.status(src.jsonObject("status"))
|
||||
|
||||
reaction = src.jsonObject("emoji_reaction")
|
||||
?.notEmpty()
|
||||
?.let { TootReaction.parseFedibird(it) }
|
||||
// pleroma unicode emoji
|
||||
?: src.string("emoji")?.let { TootReaction(name = it) }
|
||||
|
||||
// fedibird
|
||||
// https://github.com/fedibird/mastodon/blob/7974fd3c7ec11ea9f7bef4ad7f4009fff53f62af/app/serializers/rest/notification_serializer.rb#L9
|
||||
val visibilityString = when {
|
||||
src.boolean("limited") == true -> "limited"
|
||||
else -> src.string("reblog_visibility")
|
||||
}
|
||||
reblog_visibility = TootVisibility.parseMastodon(visibilityString)
|
||||
?: TootVisibility.Unknown
|
||||
}
|
||||
return TootNotification(
|
||||
json = src,
|
||||
id = id,
|
||||
type = type,
|
||||
id = EntityId.mayDefault(src.string("id")),
|
||||
type = src.stringOrThrow("type"),
|
||||
accountRef = accountRef,
|
||||
status = parser.status(src.jsonObject("note")),
|
||||
reaction = reaction,
|
||||
reblog_visibility = TootVisibility.Unknown,
|
||||
created_at = created_at,
|
||||
time_created_at = TootStatus.parseTime(created_at),
|
||||
)
|
||||
}
|
||||
|
||||
private fun tootNotificationMastodon(
|
||||
parser: TootParser,
|
||||
src: JsonObject,
|
||||
): TootNotification {
|
||||
|
||||
val created_at: String? = src.string("created_at")
|
||||
|
||||
val accountRef: TootAccountRef? =
|
||||
tootAccountRefOrNull(parser, parser.account(src.jsonObject("account")))
|
||||
|
||||
val status: TootStatus? = parser.status(src.jsonObject("status"))
|
||||
|
||||
val reaction: TootReaction? = src.jsonObject("emoji_reaction")
|
||||
?.notEmpty()
|
||||
?.let { TootReaction.parseFedibird(it) }
|
||||
?: src.string("emoji")?.let { TootReaction(name = it) } // pleroma unicode emoji
|
||||
|
||||
// fedibird
|
||||
// https://github.com/fedibird/mastodon/blob/7974fd3c7ec11ea9f7bef4ad7f4009fff53f62af/app/serializers/rest/notification_serializer.rb#L9
|
||||
val visibilityString = when {
|
||||
src.boolean("limited") == true -> "limited"
|
||||
else -> src.string("reblog_visibility")
|
||||
}
|
||||
|
||||
val reblog_visibility = TootVisibility.parseMastodon(visibilityString)
|
||||
?: TootVisibility.Unknown
|
||||
|
||||
return TootNotification(
|
||||
json = src,
|
||||
id = EntityId.mayDefault(src.string("id")),
|
||||
type = src.stringOrThrow("type"),
|
||||
accountRef = accountRef,
|
||||
status = status,
|
||||
reaction = reaction,
|
||||
reblog_visibility = reblog_visibility,
|
||||
created_at = created_at,
|
||||
time_created_at = time_created_at,
|
||||
time_created_at = TootStatus.parseTime(created_at),
|
||||
)
|
||||
}
|
||||
|
||||
fun tootNotification(parser: TootParser, src: JsonObject): TootNotification =
|
||||
when (parser.serviceType) {
|
||||
ServiceType.MISSKEY -> tootNotificationMisskey(parser, src)
|
||||
else -> tootNotificationMastodon(parser, src)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getOrderId() = id
|
||||
|
@ -172,16 +162,8 @@ class TootNotification(
|
|||
fun getNotificationLine(context: Context): String {
|
||||
|
||||
val name = when (PrefB.bpShowAcctInSystemNotification.value) {
|
||||
false -> accountRef?.decoded_display_name
|
||||
|
||||
true -> {
|
||||
val acctPretty = accountRef?.get()?.acct?.pretty
|
||||
if (acctPretty?.isNotEmpty() == true) {
|
||||
"@$acctPretty"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
true -> accountRef?.get()?.acct?.pretty?.notEmpty()?.let { "@$it" }
|
||||
else -> accountRef?.decoded_display_name
|
||||
} ?: "?"
|
||||
|
||||
return when (type) {
|
||||
|
@ -223,7 +205,10 @@ class TootNotification(
|
|||
TYPE_EMOJI_REACTION_PLEROMA,
|
||||
TYPE_EMOJI_REACTION,
|
||||
TYPE_REACTION,
|
||||
-> context.getString(R.string.display_name_reaction_by, name)
|
||||
-> arrayOf(
|
||||
context.getString(R.string.display_name_reaction_by, name),
|
||||
reaction?.name
|
||||
).mapNotNull { it.notEmpty() }.joinToString(" ")
|
||||
|
||||
TYPE_VOTE,
|
||||
TYPE_POLL_VOTE_MISSKEY,
|
||||
|
@ -239,7 +224,7 @@ class TootNotification(
|
|||
TYPE_POLL,
|
||||
-> context.getString(R.string.end_of_polling_from, name)
|
||||
|
||||
else -> "?"
|
||||
else -> context.getString(R.string.unknown_notification_from, name) + " :" + type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.annotation.StringRes
|
|||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.TootAccountMap
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRef
|
||||
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
|
||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||
import jp.juggler.subwaytooter.mfm.SpannableStringBuilderEx
|
||||
|
@ -113,9 +114,9 @@ class TootStatus(
|
|||
//Application from which the status was posted
|
||||
val application: TootApplication?,
|
||||
|
||||
var custom_emojis: HashMap<String, CustomEmoji>? = null,
|
||||
var custom_emojis: MutableMap<String, CustomEmoji>?,
|
||||
|
||||
val profile_emojis: HashMap<String, NicoProfileEmoji>?,
|
||||
val profile_emojis: Map<String, NicoProfileEmoji>?,
|
||||
|
||||
// The time the status was created
|
||||
private val created_at: String?,
|
||||
|
@ -569,12 +570,19 @@ class TootStatus(
|
|||
}
|
||||
val who = parser.account(src.jsonObject("user"))
|
||||
?: error("missing account")
|
||||
val accountRef = TootAccountRef.tootAccountRef(parser, who)
|
||||
val accountRef = tootAccountRef(parser, who)
|
||||
val account = accountRef.get()
|
||||
val created_at = src.string("createdAt")
|
||||
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
|
||||
val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
|
||||
CustomEmoji.decodeMisskey(parser.apDomain, parser.apiHost, it)
|
||||
var custom_emojis: MutableMap<String, CustomEmoji>? =
|
||||
parseMapOrNull(src.jsonArray("emojis"),CustomEmoji::decodeMisskey)
|
||||
val reactionEmojis: MutableMap<String, CustomEmoji>? =
|
||||
CustomEmoji.decodeMisskey12ReactionEmojis(src.jsonObject("reactionEmojis"))
|
||||
if (reactionEmojis != null) {
|
||||
custom_emojis = when (custom_emojis) {
|
||||
null -> reactionEmojis
|
||||
else -> (reactionEmojis + custom_emojis).toMutableMap()
|
||||
}
|
||||
}
|
||||
|
||||
// Misskeyは画像毎にNSFWフラグがある。どれか1枚でもNSFWならトゥート全体がNSFWということにする
|
||||
|
@ -941,9 +949,7 @@ class TootStatus(
|
|||
val created_at = src.string("created_at")
|
||||
|
||||
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
|
||||
val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
|
||||
CustomEmoji.decode(parser.apDomain, parser.apiHost, it)
|
||||
}
|
||||
val custom_emojis = parseMapOrNull(src.jsonArray("emojis"),CustomEmoji::decodeMastodon)
|
||||
|
||||
val profile_emojis = when (val o = src["profile_emojis"]) {
|
||||
is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) }
|
||||
|
@ -981,7 +987,7 @@ class TootStatus(
|
|||
time_created_at = parseTime(created_at)
|
||||
media_attachments =
|
||||
parseListOrNull(src.jsonArray("media_attachments")) {
|
||||
tootAttachment(parser,it)
|
||||
tootAttachment(parser, it)
|
||||
}
|
||||
val visibilityString = when {
|
||||
src.boolean("limited") == true -> "limited"
|
||||
|
|
|
@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.api.finder
|
|||
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.util.data.JsonArray
|
||||
import jp.juggler.util.data.JsonObject
|
||||
|
@ -23,7 +24,7 @@ private fun misskeyUnwrapRelationAccount(parser: TootParser, srcList: JsonArray,
|
|||
srcList.objectList().mapNotNull {
|
||||
when (val relationId = EntityId.mayNull(it.string("id"))) {
|
||||
null -> null
|
||||
else -> TootAccountRef.mayNull(parser, parser.account(it.jsonObject(key)))
|
||||
else -> tootAccountRefOrNull(parser, parser.account(it.jsonObject(key)))
|
||||
?.apply { _orderId = relationId }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -327,7 +327,7 @@ class Column(
|
|||
appState: AppState,
|
||||
accessInfo: SavedAccount,
|
||||
type: Int,
|
||||
vararg params: Any,
|
||||
params: Array<out Any>,
|
||||
) : this(
|
||||
appState = appState,
|
||||
context = appState.context,
|
||||
|
|
|
@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.api.TootApiClient
|
|||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
|
||||
import jp.juggler.subwaytooter.columnviewholder.saveScrollPosition
|
||||
import jp.juggler.util.data.*
|
||||
import jp.juggler.util.log.LogCategory
|
||||
|
@ -122,7 +123,7 @@ suspend fun Column.loadProfileAccount(
|
|||
// ユーザリレーションの取り扱いのため、別のparserを作ってはいけない
|
||||
parser.misskeyDecodeProfilePin = true
|
||||
try {
|
||||
TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
||||
tootAccountRefOrNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
||||
this.whoAccount = a
|
||||
client.publishApiProgress("") // カラムヘッダの再表示
|
||||
}
|
||||
|
@ -134,7 +135,7 @@ suspend fun Column.loadProfileAccount(
|
|||
else -> client.request(
|
||||
"/api/v1/accounts/$profileId"
|
||||
)?.also { result1 ->
|
||||
TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
||||
tootAccountRefOrNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
||||
this.whoAccount = a
|
||||
|
||||
this.whoFeaturedTags = null
|
||||
|
|
|
@ -95,21 +95,22 @@ object DlgConfirm {
|
|||
suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) =
|
||||
confirm(getString(messageId, *args))
|
||||
|
||||
suspend fun AppCompatActivity.confirm(message: CharSequence) {
|
||||
suspend fun AppCompatActivity.confirm(message: CharSequence, title: CharSequence? = null) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
try {
|
||||
val views = DlgConfirmBinding.inflate(layoutInflater)
|
||||
views.tvMessage.text = message
|
||||
views.cbSkipNext.visibility = View.GONE
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(views.root)
|
||||
.setCancelable(true)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
val dialog = AlertDialog.Builder(this).apply {
|
||||
setView(views.root)
|
||||
setCancelable(true)
|
||||
title?.let { setTitle(it) }
|
||||
setNegativeButton(R.string.cancel, null)
|
||||
setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (cont.isActive) cont.resume(Unit)
|
||||
}
|
||||
.create()
|
||||
}.create()
|
||||
dialog.setOnDismissListener {
|
||||
if (cont.isActive) cont.resumeWithException(CancellationException("dialog closed."))
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import jp.juggler.subwaytooter.pref.PrefB
|
|||
import jp.juggler.util.data.JsonArray
|
||||
import jp.juggler.util.data.JsonObject
|
||||
import jp.juggler.util.data.notEmpty
|
||||
import jp.juggler.util.data.toMutableMap
|
||||
|
||||
sealed interface EmojiBase
|
||||
|
||||
|
@ -52,7 +53,6 @@ class UnicodeEmoji(
|
|||
}
|
||||
|
||||
class CustomEmoji(
|
||||
val apDomain: Host,
|
||||
val shortcode: String, // shortcode (コロンを含まない)
|
||||
val url: String, // 画像URL
|
||||
val staticUrl: String?, // アニメーションなしの画像URL
|
||||
|
@ -63,7 +63,6 @@ class CustomEmoji(
|
|||
) : EmojiBase, Mappable<String> {
|
||||
|
||||
fun makeAlias(alias: String) = CustomEmoji(
|
||||
apDomain = apDomain,
|
||||
shortcode = shortcode,
|
||||
url = url,
|
||||
staticUrl = staticUrl,
|
||||
|
@ -80,9 +79,8 @@ class CustomEmoji(
|
|||
|
||||
companion object {
|
||||
|
||||
val decode: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src ->
|
||||
CustomEmoji(
|
||||
apDomain = apDomain,
|
||||
fun decodeMastodon(src: JsonObject): CustomEmoji {
|
||||
return CustomEmoji(
|
||||
shortcode = src.stringOrThrow("shortcode"),
|
||||
url = src.stringOrThrow("url"),
|
||||
staticUrl = src.string("static_url"),
|
||||
|
@ -91,24 +89,20 @@ class CustomEmoji(
|
|||
)
|
||||
}
|
||||
|
||||
val decodeMisskey: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src ->
|
||||
fun decodeMisskey(src: JsonObject): CustomEmoji {
|
||||
val url = src.string("url") ?: error("missing url")
|
||||
|
||||
CustomEmoji(
|
||||
apDomain = apDomain,
|
||||
return CustomEmoji(
|
||||
shortcode = src.string("name") ?: error("missing name"),
|
||||
url = url,
|
||||
staticUrl = url,
|
||||
aliases = parseAliases(src.jsonArray("aliases")),
|
||||
category = src.string("category"),
|
||||
)
|
||||
}
|
||||
|
||||
val decodeMisskey13: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, apiHost, src ->
|
||||
fun decodeMisskey13(apiHost: Host, src: JsonObject): CustomEmoji {
|
||||
val name = src.string("name") ?: error("missing name")
|
||||
val url = "https://${apiHost.ascii}/emoji/$name.webp"
|
||||
CustomEmoji(
|
||||
apDomain = apDomain,
|
||||
return CustomEmoji(
|
||||
shortcode = name,
|
||||
url = url,
|
||||
staticUrl = url,
|
||||
|
@ -117,6 +111,20 @@ class CustomEmoji(
|
|||
)
|
||||
}
|
||||
|
||||
// 入力は name→URLの単純なマップ
|
||||
fun decodeMisskey12ReactionEmojis(src: JsonObject?): MutableMap<String, CustomEmoji>? =
|
||||
src?.entries?.mapNotNull {
|
||||
val (k, v) = it
|
||||
when (val url = (v as? String)) {
|
||||
null, "" -> null
|
||||
else -> k to CustomEmoji(
|
||||
shortcode = k,
|
||||
url = url,
|
||||
staticUrl = url + (if (url.contains('?')) '&' else '?') + "static=1",
|
||||
)
|
||||
}
|
||||
}?.notEmpty()?.toMutableMap()
|
||||
|
||||
private fun parseAliases(src: JsonArray?): ArrayList<String>? {
|
||||
var dst = null as ArrayList<String>?
|
||||
if (src != null) {
|
||||
|
|
|
@ -121,7 +121,7 @@ enum class NotificationChannels(
|
|||
|
||||
;
|
||||
|
||||
fun isDissabled(context: Context) = !isEnabled(context)
|
||||
fun isDisabled(context: Context) = !isEnabled(context)
|
||||
|
||||
fun isEnabled(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
|
@ -175,22 +175,26 @@ enum class NotificationChannels(
|
|||
text: String? = context.getString(descId),
|
||||
piTap: PendingIntent? = null,
|
||||
piDelete: PendingIntent? = null,
|
||||
force:Boolean = false,
|
||||
): ForegroundInfo? {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
log.w("[$id] missing POST_NOTIFICATIONS.")
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
if(!force){
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
log.w("[$id] missing POST_NOTIFICATIONS.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (!notificationManager.isChannelEnabled(id)) {
|
||||
log.w("[$id] notification channel is disabled.")
|
||||
return null
|
||||
}
|
||||
}
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
if (!notificationManager.isChannelEnabled(id)) {
|
||||
log.w("[$id] notification channel is disabled.")
|
||||
return null
|
||||
}
|
||||
val nc = this
|
||||
val builder = NotificationCompat.Builder(context, nc.id).apply {
|
||||
priority = nc.priority
|
||||
|
|
|
@ -328,6 +328,13 @@ class PollingChecker(
|
|||
|
||||
val notification = parser.notification(src) ?: return
|
||||
|
||||
// プッシュ通知で既出なら通知しない
|
||||
// プルの場合同じ通知が何度もここを通るので、既出フラグを立てない
|
||||
if (daoNotificationShown.isDuplicate(account.acct, notification.id.toString())) {
|
||||
log.i("update_sub: skip duplicate. ${account.acct} ${notification.id}")
|
||||
return
|
||||
}
|
||||
|
||||
// アプリミュートと単語ミュート
|
||||
if (notification.status?.checkMuted() == true) return
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ fun AppCompatActivity.resetNotificationTracking(account: SavedAccount) {
|
|||
}
|
||||
launchAndShowError {
|
||||
withContext(AppDispatchers.IO){
|
||||
daoNotificationShown.cleayByAcct(account.acct.ascii)
|
||||
daoNotificationShown.cleayByAcct(account.acct)
|
||||
PollingChecker.accountMutex(account.db_id).withLock {
|
||||
daoNotificationTracking.resetTrackingState(account.db_id)
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ suspend fun checkNoticifationAll(
|
|||
}
|
||||
}
|
||||
|
||||
daoSavedAccount.loadAccountList().mapNotNull { sa ->
|
||||
daoSavedAccount.loadRealAccounts().mapNotNull { sa ->
|
||||
when {
|
||||
sa.isPseudo || !sa.isConfirmed -> null
|
||||
else -> EmptyScope.launch(AppDispatchers.DEFAULT) {
|
||||
|
|
|
@ -158,9 +158,10 @@ class PollingWorker2(
|
|||
|
||||
private fun messageToForegroundInfo(
|
||||
text: String,
|
||||
force:Boolean =false
|
||||
): ForegroundInfo? {
|
||||
// テキストが変化していないなら更新しない
|
||||
if (text.isEmpty() || text == lastMessage) return null
|
||||
if (!force && (text.isEmpty() || text == lastMessage)) return null
|
||||
|
||||
lastMessage = text
|
||||
log.i(text)
|
||||
|
@ -181,6 +182,15 @@ class PollingWorker2(
|
|||
context,
|
||||
text = text,
|
||||
piTap = piTap,
|
||||
force = force,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* ワーカーの初期化時にOSから呼ばれる場合がある
|
||||
* - Android 11 moto g31 で発生
|
||||
* - ダミーメッセージを仕込んだForegroundInfoを返す
|
||||
*/
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo =
|
||||
messageToForegroundInfo("initializing…",force=true)!!
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ object PrefB {
|
|||
)
|
||||
val bpMoveNotificationsQuickFilter = BooleanPref(
|
||||
"MoveNotificationsQuickFilter",
|
||||
false
|
||||
true
|
||||
)
|
||||
val bpShowAcctInSystemNotification = BooleanPref(
|
||||
"ShowAcctInSystemNotification",
|
||||
|
|
|
@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.push
|
|||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.entity.TootNotification
|
||||
import jp.juggler.subwaytooter.table.PushMessage
|
||||
import jp.juggler.util.log.LogCategory
|
||||
|
||||
|
@ -14,42 +15,42 @@ enum class PushMessageIconColor(
|
|||
val keys: Array<String>,
|
||||
) {
|
||||
Favourite(
|
||||
R.color.colorNotificationAccentFavourite,
|
||||
0,
|
||||
R.drawable.ic_star_outline,
|
||||
arrayOf("favourite"),
|
||||
),
|
||||
Mention(
|
||||
R.color.colorNotificationAccentMention,
|
||||
0,
|
||||
R.drawable.outline_alternate_email_24,
|
||||
arrayOf("mention"),
|
||||
),
|
||||
Reply(
|
||||
R.color.colorNotificationAccentReply,
|
||||
0,
|
||||
R.drawable.ic_reply,
|
||||
arrayOf("reply")
|
||||
),
|
||||
Reblog(
|
||||
R.color.colorNotificationAccentReblog,
|
||||
0,
|
||||
R.drawable.ic_repeat,
|
||||
arrayOf("reblog", "renote"),
|
||||
),
|
||||
Quote(
|
||||
R.color.colorNotificationAccentQuote,
|
||||
0,
|
||||
R.drawable.ic_quote,
|
||||
arrayOf("quote"),
|
||||
),
|
||||
Follow(
|
||||
R.color.colorNotificationAccentFollow,
|
||||
0,
|
||||
R.drawable.ic_person_add,
|
||||
arrayOf("follow", "followRequestAccepted")
|
||||
),
|
||||
Unfollow(
|
||||
R.color.colorNotificationAccentUnfollow,
|
||||
0,
|
||||
R.drawable.ic_follow_cross,
|
||||
arrayOf("unfollow")
|
||||
),
|
||||
Reaction(
|
||||
R.color.colorNotificationAccentReaction,
|
||||
0,
|
||||
R.drawable.outline_add_reaction_24,
|
||||
arrayOf("reaction", "emoji_reaction", "pleroma:emoji_reaction")
|
||||
),
|
||||
|
@ -59,25 +60,30 @@ enum class PushMessageIconColor(
|
|||
arrayOf("follow_request", "receiveFollowRequest"),
|
||||
),
|
||||
Poll(
|
||||
R.color.colorNotificationAccentPoll,
|
||||
0,
|
||||
R.drawable.outline_poll_24,
|
||||
arrayOf("pollVote", "poll_vote", "poll"),
|
||||
),
|
||||
Status(
|
||||
R.color.colorNotificationAccentStatus,
|
||||
0,
|
||||
R.drawable.ic_edit,
|
||||
arrayOf("status", "update", "status_reference")
|
||||
),
|
||||
SignUp(
|
||||
R.color.colorNotificationAccentSignUp,
|
||||
AdminSignUp(
|
||||
0,
|
||||
R.drawable.outline_group_add_24,
|
||||
arrayOf("admin.sign_up"),
|
||||
arrayOf(TootNotification.TYPE_ADMIN_SIGNUP),
|
||||
),
|
||||
AdminReport(
|
||||
R.color.colorNotificationAccentAdminReport,
|
||||
R.drawable.ic_error,
|
||||
arrayOf(TootNotification.TYPE_ADMIN_REPORT),
|
||||
),
|
||||
|
||||
Unknown(
|
||||
R.color.colorNotificationAccentUnknown,
|
||||
R.drawable.ic_question,
|
||||
arrayOf("unknown", "admin.sign_up"),
|
||||
arrayOf("unknown"),
|
||||
)
|
||||
;
|
||||
|
||||
|
|
|
@ -7,8 +7,10 @@ import jp.juggler.crypt.generateKeyPair
|
|||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.ApiError
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.TootAccount.Companion.tootAccount
|
||||
import jp.juggler.subwaytooter.api.entity.TootNotification
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.api.entity.parseItem
|
||||
import jp.juggler.subwaytooter.api.push.ApiPushMisskey
|
||||
import jp.juggler.subwaytooter.pref.PrefDevice
|
||||
import jp.juggler.subwaytooter.pref.lazyContext
|
||||
|
@ -172,11 +174,28 @@ class PushMisskey(
|
|||
|
||||
when (val eventType = json.string("type")) {
|
||||
"notification" -> {
|
||||
val body = json.jsonObject("body")
|
||||
?: error("missing body of notification")
|
||||
val parser = TootParser(context, a)
|
||||
val notification = parser.notification(json.jsonObject("body"))
|
||||
?: error("can't parse notification. json=$json")
|
||||
|
||||
val user = notification.account
|
||||
val whoJson = body.jsonObject("user")
|
||||
var who = parseItem(whoJson) { tootAccount(parser, it) }
|
||||
|
||||
body.jsonObject("note")?.let { noteJson ->
|
||||
if (noteJson["user"] == null) {
|
||||
noteJson["user"] = when (noteJson.string("userId")) {
|
||||
null, "" -> null
|
||||
who?.id?.toString() -> whoJson
|
||||
a.loginAccount?.id?.toString() -> a.loginAccount?.json
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val notification = parser.notification(body)
|
||||
?: error("can't parse notification. json=$body")
|
||||
|
||||
who = notification.account
|
||||
|
||||
// アプリミュートと単語ミュート
|
||||
if (notification.status?.checkMuted() == true) {
|
||||
|
@ -191,7 +210,7 @@ class PushMisskey(
|
|||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||
-> {
|
||||
val whoAcct = a.getFullAcct(user)
|
||||
val whoAcct = a.getFullAcct(who)
|
||||
if (TootStatus.favMuteSet?.contains(whoAcct) == true) {
|
||||
error("muted by favMuteSet ${whoAcct.pretty}")
|
||||
}
|
||||
|
@ -200,8 +219,9 @@ class PushMisskey(
|
|||
|
||||
// バッジ画像のURLはない。通知種別により決まる
|
||||
pm.iconSmall = null
|
||||
pm.iconLarge = a.supplyBaseUrl(user?.avatar_static)
|
||||
pm.iconLarge = a.supplyBaseUrl(who?.avatar_static)
|
||||
pm.notificationType = notification.type
|
||||
pm.notificationId = notification.id.toString()
|
||||
|
||||
json.long("dateTime")?.let { pm.timestamp = it }
|
||||
|
||||
|
@ -224,3 +244,67 @@ class PushMisskey(
|
|||
}
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
Misskey13
|
||||
{
|
||||
"type": "notification",
|
||||
"body": {
|
||||
"id": "9ayflq5wj4",
|
||||
"createdAt": "2023-02-07T23:22:38.132Z",
|
||||
"type": "reaction",
|
||||
"isRead": false,
|
||||
"userId": "80jbzppr37",
|
||||
"user": {
|
||||
"id": "80jbzppr37",
|
||||
"name": "tateisu🔧",
|
||||
"username": "tateisu",
|
||||
"host": "fedibird.com",
|
||||
"avatarUrl": "https://nos3.arkjp.net/avatar.webp?url=https%3A%2F%2Fs3.fedibird.com%2Faccounts%2Favatars%2F000%2F010%2F223%2Foriginal%2Fb7ace6ef7eaaf49f.png&avatar=1",
|
||||
"avatarBlurhash": "yMMHS-t71NWX~qx]%2yEf6i_kCoKn%M{tSkCoJaeM{ayoeyEWBxtt7IAWBWqShkCi_WBt7jZRkMxayt6aeWray%Mxvj[oeofM|WBRj",
|
||||
"isBot": false,
|
||||
"isCat": false,
|
||||
"instance": {
|
||||
"name": "Fedibird",
|
||||
"softwareName": "fedibird",
|
||||
"softwareVersion": "0.1",
|
||||
"iconUrl": "https://fedibird.com/android-chrome-192x192.png",
|
||||
"faviconUrl": "https://fedibird.com/favicon.ico",
|
||||
"themeColor": "#282c37"
|
||||
},
|
||||
"emojis": {},
|
||||
"onlineStatus": "unknown"
|
||||
},
|
||||
"note": {
|
||||
"id": "9aybef5b1d",
|
||||
"createdAt": "2023-02-07T21:24:58.799Z",
|
||||
"userId": "7rm6y6thc1",
|
||||
"text": "(📎1)",
|
||||
"visibility": "public",
|
||||
"localOnly": false,
|
||||
"renoteCount": 0,
|
||||
"repliesCount": 0,
|
||||
"reactions": {
|
||||
"👍": 1,
|
||||
":kakkoii@.:": 1,
|
||||
":utsukushii@.:": 1
|
||||
},
|
||||
"reactionEmojis": {
|
||||
"blobcatlobster_MUDAMUDAMUDA@fedibird.com": "https://nos3.arkjp.net/emoji.webp?url=https%3A%2F%2Fs3.fedibird.com%2Fcustom_emojis%2Fimages%2F000%2F151%2F856%2Foriginal%2F936dd0a34673cb19.png"
|
||||
},
|
||||
"fileIds": [
|
||||
"9aybedosdl"
|
||||
],
|
||||
"files": [...],
|
||||
],
|
||||
"replyId": null,
|
||||
"renoteId": null
|
||||
},
|
||||
"reaction": "👍"
|
||||
},
|
||||
"userId": "7rm6y6thc1",
|
||||
"dateTime": 1675812160174
|
||||
}
|
||||
|
||||
|
||||
*/
|
|
@ -12,7 +12,6 @@ import androidx.work.await
|
|||
import jp.juggler.crypt.*
|
||||
import jp.juggler.subwaytooter.ActCallback
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.api.entity.Acct
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.api.push.ApiPushAppServer
|
||||
import jp.juggler.subwaytooter.api.push.ApiPushMastodon
|
||||
|
@ -46,19 +45,23 @@ import java.util.concurrent.TimeUnit
|
|||
|
||||
private val log = LogCategory("PushRepo")
|
||||
|
||||
private val defaultOkHttp by lazy {
|
||||
OkHttpClient.Builder().apply {
|
||||
connectTimeout(60, TimeUnit.SECONDS)
|
||||
writeTimeout(60, TimeUnit.SECONDS)
|
||||
readTimeout(60, TimeUnit.SECONDS)
|
||||
}.build()
|
||||
}
|
||||
|
||||
val Context.pushRepo: PushRepo
|
||||
get() {
|
||||
val okHttp = OkHttpClient.Builder().apply {
|
||||
connectTimeout(60, TimeUnit.SECONDS)
|
||||
writeTimeout(60, TimeUnit.SECONDS)
|
||||
readTimeout(60, TimeUnit.SECONDS)
|
||||
}.build()
|
||||
val okHttp = defaultOkHttp
|
||||
val appDatabase = appDatabase
|
||||
return PushRepo(
|
||||
context = applicationContextSafe,
|
||||
apiPushAppServer = ApiPushAppServer(okHttp),
|
||||
apiPushMastodon = ApiPushMastodon(okHttp),
|
||||
apiPushMisskey = ApiPushMisskey(okHttp),
|
||||
apiAppServer = ApiPushAppServer(okHttp),
|
||||
apiMastodon = ApiPushMastodon(okHttp),
|
||||
apiMisskey = ApiPushMisskey(okHttp),
|
||||
daoSavedAccount = SavedAccount.Access(appDatabase, this),
|
||||
daoPushMessage = PushMessage.Access(appDatabase),
|
||||
daoStatus = AccountNotificationStatus.Access(appDatabase),
|
||||
|
@ -70,9 +73,9 @@ val Context.pushRepo: PushRepo
|
|||
|
||||
class PushRepo(
|
||||
private val context: Context,
|
||||
private val apiPushMastodon: ApiPushMastodon,
|
||||
private val apiPushMisskey: ApiPushMisskey,
|
||||
private val apiPushAppServer: ApiPushAppServer,
|
||||
private val apiMastodon: ApiPushMastodon,
|
||||
private val apiMisskey: ApiPushMisskey,
|
||||
private val apiAppServer: ApiPushAppServer,
|
||||
private val daoSavedAccount: SavedAccount.Access,
|
||||
private val daoPushMessage: PushMessage.Access,
|
||||
private val daoStatus: AccountNotificationStatus.Access,
|
||||
|
@ -92,7 +95,7 @@ class PushRepo(
|
|||
private val pushMisskey by lazy {
|
||||
PushMisskey(
|
||||
context = context,
|
||||
api = apiPushMisskey,
|
||||
api = apiMisskey,
|
||||
provider = provider,
|
||||
prefDevice = prefDevice,
|
||||
daoStatus = daoStatus,
|
||||
|
@ -102,7 +105,7 @@ class PushRepo(
|
|||
private val pushMastodon by lazy {
|
||||
PushMastodon(
|
||||
context = context,
|
||||
api = apiPushMastodon,
|
||||
api = apiMastodon,
|
||||
provider = provider,
|
||||
prefDevice = prefDevice,
|
||||
daoStatus = daoStatus,
|
||||
|
@ -220,7 +223,7 @@ class PushRepo(
|
|||
prefDevice.fcmTokenExpired.notEmpty()?.let {
|
||||
refReporter?.get()?.setMessage("期限切れのFCMデバイストークンをアプリサーバから削除しています")
|
||||
log.i("remove fcmTokenExpired")
|
||||
apiPushAppServer.endpointRemove(fcmToken = it)
|
||||
apiAppServer.endpointRemove(fcmToken = it)
|
||||
prefDevice.fcmTokenExpired = null
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
|
@ -232,15 +235,15 @@ class PushRepo(
|
|||
prefDevice.upEndpointExpired.notEmpty()?.let {
|
||||
refReporter?.get()?.setMessage("期限切れのUnifiedPushエンドポイントをアプリサーバから削除しています")
|
||||
log.i("remove upEndpointExpired")
|
||||
apiPushAppServer.endpointRemove(upUrl = it)
|
||||
apiAppServer.endpointRemove(upUrl = it)
|
||||
prefDevice.upEndpointExpired = null
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "can't forgot upEndpointExpired")
|
||||
}
|
||||
|
||||
val realAccounts = daoSavedAccount.loadAccountList()
|
||||
.filter { !it.isPseudo }
|
||||
val realAccounts = daoSavedAccount.loadRealAccounts()
|
||||
.filter { !it.isPseudo && it.isConfirmed }
|
||||
|
||||
val accts = realAccounts.map { it.acct }
|
||||
|
||||
|
@ -269,23 +272,34 @@ class PushRepo(
|
|||
prefDevice.pushDistributor = null
|
||||
}
|
||||
|
||||
log.i("pushDistributor=${prefDevice.pushDistributor}")
|
||||
val acctHashList = acctHashMap.keys.toList()
|
||||
|
||||
val json = when (prefDevice.pushDistributor) {
|
||||
null, "" -> when {
|
||||
fcmHandler.hasFcm -> registerEndpointFcm(acctHashList)
|
||||
fcmHandler.hasFcm -> {
|
||||
log.i("registerEndpoint dist=FCM(default), acctHashList=${acctHashList.size}")
|
||||
registerEndpointFcm(acctHashList)
|
||||
}
|
||||
else -> {
|
||||
log.w("pushDistributor not selected. but can't select default distributor from background service.")
|
||||
null
|
||||
return
|
||||
}
|
||||
}
|
||||
PrefDevice.PUSH_DISTRIBUTOR_NONE -> {
|
||||
log.i("push distrobuter 'none' is selected. it will remove subscription.")
|
||||
willRemoveSubscription = true
|
||||
null
|
||||
}
|
||||
PrefDevice.PUSH_DISTRIBUTOR_FCM -> registerEndpointFcm(acctHashList)
|
||||
else -> registerEndpointUnifiedPush(acctHashList)
|
||||
PrefDevice.PUSH_DISTRIBUTOR_FCM -> {
|
||||
log.i("registerEndpoint dist=FCM, acctHashList=${acctHashList.size}")
|
||||
registerEndpointFcm(acctHashList)
|
||||
}
|
||||
else -> {
|
||||
log.i("registerEndpoint dist=${prefDevice.pushDistributor}, acctHashList=${acctHashList.size}")
|
||||
registerEndpointUnifiedPush(acctHashList)
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
json.isNullOrEmpty() ->
|
||||
log.i("no information of appServerHash.")
|
||||
|
@ -355,8 +369,7 @@ class PushRepo(
|
|||
null
|
||||
}
|
||||
else -> {
|
||||
log.i("endpointUpsert up ")
|
||||
apiPushAppServer.endpointUpsert(
|
||||
apiAppServer.endpointUpsert(
|
||||
upUrl = upEndpoint,
|
||||
fcmToken = null,
|
||||
acctHashList = acctHashList
|
||||
|
@ -371,8 +384,7 @@ class PushRepo(
|
|||
null
|
||||
}
|
||||
else -> {
|
||||
log.i("endpointUpsert fcm ")
|
||||
apiPushAppServer.endpointUpsert(
|
||||
apiAppServer.endpointUpsert(
|
||||
upUrl = null,
|
||||
fcmToken = fcmToken,
|
||||
acctHashList = acctHashList
|
||||
|
@ -436,7 +448,7 @@ class PushRepo(
|
|||
*
|
||||
* - 実際のアプリでは解読できたものだけを保存したいが、これは試験アプリなので…
|
||||
*/
|
||||
suspend fun reDecode(pm: PushMessage) {
|
||||
suspend fun reprocess(pm: PushMessage) {
|
||||
withContext(AppDispatchers.IO) {
|
||||
updateMessage(pm.id, allowDupilicateNotification = true)
|
||||
}
|
||||
|
@ -463,7 +475,7 @@ class PushRepo(
|
|||
// アプリサーバから読み直す
|
||||
if (map["b"] == null) {
|
||||
map.string("l")?.let { largeObjectId ->
|
||||
apiPushAppServer.getLargeObject(largeObjectId)
|
||||
apiAppServer.getLargeObject(largeObjectId)
|
||||
?.let {
|
||||
map = it.decodeBinPack() as? BinPackMap
|
||||
?: error("binPack decode failed.")
|
||||
|
@ -479,14 +491,14 @@ class PushRepo(
|
|||
val status = daoStatus.findByAcctHash(acctHash)
|
||||
?: error("missing status for acctHash $acctHash")
|
||||
|
||||
val acct = status.acct.notEmpty()
|
||||
val acct = status.acct.takeIf { it.isValidFull }
|
||||
?: error("empty acct.")
|
||||
|
||||
val account = daoSavedAccount.loadAccountByAcct(Acct.parse(acct))
|
||||
?: error("missing account for acct ${status.acct}")
|
||||
|
||||
pm.loginAcct = status.acct
|
||||
|
||||
val account = daoSavedAccount.loadAccountByAcct(acct)
|
||||
?: error("missing account for acct ${status.acct}")
|
||||
|
||||
decodeMessageContent(status, pm, map)
|
||||
val messageJson = pm.messageJson
|
||||
|
||||
|
@ -496,7 +508,7 @@ class PushRepo(
|
|||
// メッセージに含まれるappServerHashを指定してendpoint登録を削除する
|
||||
// するとアプリサーバはSNSサーバに対してgoneを返すようになり掃除が適切に行われるはず
|
||||
map.string("c").notEmpty()?.let {
|
||||
val count = apiPushAppServer.endpointRemove(hashId = it).int("count")
|
||||
val count = apiAppServer.endpointRemove(hashId = it).int("count")
|
||||
log.w("endpointRemove $count hashId=$it")
|
||||
}
|
||||
|
||||
|
@ -504,29 +516,37 @@ class PushRepo(
|
|||
}
|
||||
|
||||
// Mastodonはなぜかアクセストークンが書いてあるので危険…
|
||||
val censored = messageJson.toString()
|
||||
val messageJsonFiltered = messageJson.toString()
|
||||
.replace(
|
||||
""""access_token":"[^"]+"""".toRegex(),
|
||||
""""access_token":"***""""
|
||||
)
|
||||
log.i("${status.acct} $censored")
|
||||
log.i("${status.acct} $messageJsonFiltered")
|
||||
|
||||
// ミュート用データを時々読む
|
||||
TootStatus.updateMuteData()
|
||||
|
||||
// messageJsonを解釈して通知に出す内容を決める
|
||||
TootStatus.updateMuteData()
|
||||
pushBase(account).formatPushMessage(account, pm)
|
||||
|
||||
val notificationId = pm.notificationId
|
||||
if (notificationId.isNullOrEmpty()) {
|
||||
error("can't show notification. missing notificationId.")
|
||||
log.w("can't show notification. missing notificationId.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!account.canNotificationShowing(pm.notificationType)) {
|
||||
log.w("notificationType ${pm.notificationType} is disabled.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!allowDupilicateNotification &&
|
||||
daoNotificationShown.duplicateOrPut(acct, notificationId)
|
||||
) {
|
||||
error("can't show notification. it's duplicate. $acct $notificationId")
|
||||
log.w("can't show notification. it's duplicate. $acct $notificationId")
|
||||
return
|
||||
}
|
||||
|
||||
// 解読できた(例外が出なかった)なら通知を出す
|
||||
showPushNotification(pm, account, notificationId)
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "updateMessage failed.")
|
||||
|
@ -620,8 +640,8 @@ class PushRepo(
|
|||
account: SavedAccount,
|
||||
notificationId: String,
|
||||
) {
|
||||
if (ncPushMessage.isDissabled(context)) {
|
||||
log.w("ncPushMessage isDissabled.")
|
||||
if (ncPushMessage.isDisabled(context)) {
|
||||
log.w("ncPushMessage isDisabled.")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -672,14 +692,18 @@ class PushRepo(
|
|||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
// val iTap = intentActMessage(pm.messageDbId)
|
||||
// val iTap = intentActMessage(pm.messageDbId)
|
||||
// val piTap = PendingIntent.getActivity(this, nc.pircTap, iTap, PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
ncPushMessage.notify(context, urlDelete) {
|
||||
color = ContextCompat.getColor(context, iconAndColor.colorRes)
|
||||
color = pm.iconColor().colorRes.notZero()
|
||||
?.let { ContextCompat.getColor(context, it) }
|
||||
?: account.notificationAccentColor.notZero()
|
||||
?: ContextCompat.getColor(context, R.color.colorOsNotificationAccent)
|
||||
|
||||
setSmallIcon(iconSmall)
|
||||
iconBitmapLarge?.let { setLargeIcon(it) }
|
||||
setContentTitle(pm.loginAcct)
|
||||
setContentTitle(pm.loginAcct?.pretty)
|
||||
setContentText(pm.text)
|
||||
setWhen(pm.timestamp)
|
||||
setContentIntent(piTap)
|
||||
|
@ -688,6 +712,8 @@ class PushRepo(
|
|||
pm.textExpand.notEmpty()?.let {
|
||||
setStyle(NotificationCompat.BigTextStyle().bigText(it))
|
||||
}
|
||||
|
||||
setGroup(context.packageName + ":" + account.acct.ascii)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package jp.juggler.subwaytooter.push
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.work.*
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.notification.NotificationChannels
|
||||
import jp.juggler.util.coroutine.AppDispatchers
|
||||
import jp.juggler.util.data.notEmpty
|
||||
|
@ -15,6 +18,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||
companion object {
|
||||
private val log = LogCategory("PushWorker")
|
||||
|
||||
private val ncPushWorker = NotificationChannels.PushWorker
|
||||
|
||||
const val KEY_ACTION = "action"
|
||||
const val KEY_ENDPOINT = "endpoint"
|
||||
const val KEY_MESSAGE_ID = "messageId"
|
||||
|
@ -31,8 +36,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||
|
||||
fun enqueueUpEndpoint(context: Context, endpoint: String) {
|
||||
workDataOf(
|
||||
PushWorker.KEY_ACTION to PushWorker.ACTION_UP_ENDPOINT,
|
||||
PushWorker.KEY_ENDPOINT to endpoint,
|
||||
KEY_ACTION to ACTION_UP_ENDPOINT,
|
||||
KEY_ENDPOINT to endpoint,
|
||||
).launchPushWorker(context)
|
||||
}
|
||||
|
||||
|
@ -45,8 +50,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||
|
||||
fun enqueuePushMessage(context: Context, messageId: Long) {
|
||||
workDataOf(
|
||||
PushWorker.KEY_ACTION to PushWorker.ACTION_MESSAGE,
|
||||
PushWorker.KEY_MESSAGE_ID to messageId,
|
||||
KEY_ACTION to ACTION_MESSAGE,
|
||||
KEY_MESSAGE_ID to messageId,
|
||||
).launchPushWorker(context)
|
||||
}
|
||||
|
||||
|
@ -68,9 +73,7 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||
}
|
||||
|
||||
override suspend fun doWork(): Result = try {
|
||||
NotificationChannels.PushWorker.createForegroundInfo(
|
||||
applicationContext,
|
||||
)?.let{setForegroundAsync(it)}
|
||||
createForegroundInfo()?.let { setForegroundAsync(it) }
|
||||
withContext(AppDispatchers.IO) {
|
||||
val pushRepo = applicationContext.pushRepo
|
||||
when (val action = inputData.getString(KEY_ACTION)) {
|
||||
|
@ -80,7 +83,7 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||
val endpoint = inputData.getString(KEY_ENDPOINT)
|
||||
?.notEmpty() ?: error("missing endpoint.")
|
||||
pushRepo.newUpEndpoint(endpoint)
|
||||
}finally{
|
||||
} finally {
|
||||
timeEndUpEndpoint.set(System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
@ -89,7 +92,7 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||
try {
|
||||
val keepAliveMode = inputData.getBoolean(KEY_KEEP_ALIVE_MODE, false)
|
||||
pushRepo.registerEndpoint(keepAliveMode)
|
||||
}finally{
|
||||
} finally {
|
||||
timeEndRegisterEndpoint.set(System.currentTimeMillis())
|
||||
}
|
||||
}
|
||||
|
@ -106,4 +109,32 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
|||
log.e(ex, "doWork failed.")
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
/**
|
||||
* 時々OSに呼ばれる
|
||||
* Android 11 moto g31 で発生
|
||||
*/
|
||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||
return createForegroundInfo(force = true)!!
|
||||
}
|
||||
|
||||
private fun createForegroundInfo(force: Boolean = false): ForegroundInfo? {
|
||||
val context = applicationContext
|
||||
|
||||
// 通知タップ時のPendingIntent
|
||||
val iTap = Intent(context, ActMain::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
val piTap = PendingIntent.getActivity(
|
||||
context,
|
||||
ncPushWorker.pircTap,
|
||||
iTap,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
return ncPushWorker.createForegroundInfo(
|
||||
context,
|
||||
piTap = piTap,
|
||||
force = force,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,23 @@ import android.graphics.Canvas
|
|||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.style.ReplacementSpan
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.core.content.ContextCompat
|
||||
import jp.juggler.apng.ApngFrames
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
import jp.juggler.subwaytooter.pref.lazyContext
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class NetworkEmojiSpan internal constructor(
|
||||
private val url: String,
|
||||
private val scale: Float = 1f,
|
||||
@DrawableRes private val errorDrawableId: Int = R.drawable.outline_broken_image_24,
|
||||
) : ReplacementSpan(), AnimatableSpan {
|
||||
|
||||
companion object {
|
||||
|
@ -25,20 +31,19 @@ class NetworkEmojiSpan internal constructor(
|
|||
private const val descentRatio = 0.211f
|
||||
}
|
||||
|
||||
private val mPaint = Paint()
|
||||
private val mPaint = Paint().apply { isFilterBitmap = true }
|
||||
private val rectSrc = Rect()
|
||||
private val rectDst = RectF()
|
||||
|
||||
// フレーム探索結果を格納する構造体を確保しておく
|
||||
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
||||
|
||||
init {
|
||||
mPaint.isFilterBitmap = true
|
||||
}
|
||||
|
||||
private var invalidateCallback: AnimatableSpanInvalidator? = null
|
||||
|
||||
private var refDrawTarget: WeakReference<Any>? = null
|
||||
|
||||
private var errorDrawableCache: Drawable? = null
|
||||
|
||||
override fun setInvalidateCallback(
|
||||
drawTargetTag: Any,
|
||||
invalidateCallback: AnimatableSpanInvalidator,
|
||||
|
@ -77,16 +82,26 @@ class NetworkEmojiSpan internal constructor(
|
|||
bottom: Int,
|
||||
textPaint: Paint,
|
||||
) {
|
||||
if (drawFrame(canvas, x, baseline, textPaint)) return
|
||||
drawError(canvas, x, baseline, textPaint)
|
||||
}
|
||||
|
||||
private fun drawFrame(
|
||||
canvas: Canvas,
|
||||
x: Float,
|
||||
baseline: Int,
|
||||
textPaint: Paint,
|
||||
): Boolean {
|
||||
val invalidateCallback = this.invalidateCallback
|
||||
if (invalidateCallback == null) {
|
||||
log.e("draw: invalidate_callback is null.")
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
// APNGデータの取得
|
||||
val frames = App1.custom_emoji_cache.getFrames(refDrawTarget, url) {
|
||||
invalidateCallback.delayInvalidate(0L)
|
||||
} ?: return
|
||||
} ?: return false
|
||||
|
||||
val t = when {
|
||||
PrefB.bpDisableEmojiAnimation.value -> 0L
|
||||
|
@ -99,13 +114,13 @@ class NetworkEmojiSpan internal constructor(
|
|||
val b = mFrameFindResult.bitmap
|
||||
if (b == null || b.isRecycled) {
|
||||
log.e("draw: bitmap is null or recycled.")
|
||||
return
|
||||
return false
|
||||
}
|
||||
val srcWidth = b.width
|
||||
val srcHeight = b.height
|
||||
if (srcWidth < 1 || srcHeight < 1) {
|
||||
log.e("draw: bitmap size is too small.")
|
||||
return
|
||||
return false
|
||||
}
|
||||
rectSrc.set(0, 0, srcWidth, srcHeight)
|
||||
|
||||
|
@ -155,5 +170,67 @@ class NetworkEmojiSpan internal constructor(
|
|||
if (delay != Long.MAX_VALUE && !PrefB.bpDisableEmojiAnimation.value) {
|
||||
invalidateCallback.delayInvalidate(delay)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun drawError(
|
||||
canvas: Canvas,
|
||||
x: Float,
|
||||
baseline: Int,
|
||||
textPaint: Paint,
|
||||
) {
|
||||
val drawable = errorDrawableCache
|
||||
?: ContextCompat.getDrawable(lazyContext, errorDrawableId)
|
||||
?.also { errorDrawableCache = it }
|
||||
|
||||
drawable ?: return
|
||||
val srcWidth = drawable.intrinsicWidth
|
||||
val srcHeight = drawable.intrinsicHeight
|
||||
|
||||
// 絵文字の正方形のサイズ
|
||||
val dstSize = textPaint.textSize * scaleRatio * scale
|
||||
|
||||
// ベースラインから上下方向にずらすオフセット
|
||||
val cDescent = dstSize * descentRatio
|
||||
val transY = baseline - dstSize + cDescent
|
||||
|
||||
// 絵文字のアスペクト比から描画範囲の幅と高さを決める
|
||||
val dstWidth: Float
|
||||
val dstHeight: Float
|
||||
val aspectSrc = srcWidth.toFloat() / srcHeight.toFloat()
|
||||
if (aspectSrc >= 1f) {
|
||||
dstWidth = dstSize
|
||||
dstHeight = dstSize / aspectSrc
|
||||
} else {
|
||||
dstHeight = dstSize
|
||||
dstWidth = dstSize * aspectSrc
|
||||
}
|
||||
val dstX = (dstSize - dstWidth) / 2f
|
||||
val dstY = (dstSize - dstHeight) / 2f
|
||||
// rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight)
|
||||
canvas.save()
|
||||
try {
|
||||
canvas.translate(x, transY)
|
||||
drawable.setBounds(
|
||||
dstX.toInt(),
|
||||
dstY.toInt(),
|
||||
(dstX + dstWidth).toInt(),
|
||||
(dstY + dstHeight).toInt()
|
||||
)
|
||||
drawable.draw(canvas)
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "drawBitmap failed.")
|
||||
|
||||
// 10月6日 18:18(アプリのバージョン: 378) Sony Xperia X Compact(F5321), Android 8.0
|
||||
// 10月6日 11:35(アプリのバージョン: 380) Samsung Galaxy S7 Edge(hero2qltetmo), Android 8.0
|
||||
// 10月2日 21:56(アプリのバージョン: 376) Google Pixel 3(blueline), Android 9
|
||||
// java.lang.RuntimeException:
|
||||
// at android.graphics.BaseCanvas.throwIfCannotDraw (BaseCanvas.java:55)
|
||||
// at android.view.DisplayListCanvas.throwIfCannotDraw (DisplayListCanvas.java:226)
|
||||
// at android.view.RecordingCanvas.drawBitmap (RecordingCanvas.java:123)
|
||||
// at jp.juggler.subwaytooter.span.NetworkEmojiSpan.draw (NetworkEmojiSpan.kt:137)
|
||||
} finally {
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import jp.juggler.subwaytooter.api.TootApiCallback
|
|||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.entity.MisskeyNoteUpdate.Companion.misskeyNoteUpdate
|
||||
import jp.juggler.subwaytooter.column.onStatusRemoved
|
||||
import jp.juggler.subwaytooter.column.reloadFilter
|
||||
import jp.juggler.subwaytooter.column.replaceStatus
|
||||
|
@ -201,13 +202,7 @@ class StreamConnection(
|
|||
log.e("$name handleMisskeyMessage: noteUpdated body is null")
|
||||
return
|
||||
}
|
||||
fireNoteUpdated(
|
||||
MisskeyNoteUpdate(
|
||||
acctGroup.account.apDomain,
|
||||
acctGroup.account.apiHost,
|
||||
body
|
||||
), channelId
|
||||
)
|
||||
fireNoteUpdated(misskeyNoteUpdate(body), channelId)
|
||||
}
|
||||
|
||||
"notification" -> {
|
||||
|
|
|
@ -12,7 +12,7 @@ class AccountNotificationStatus(
|
|||
// DB上のID
|
||||
var id: Long = 0L,
|
||||
// 該当ユーザのacct
|
||||
var acct: String = "",
|
||||
var acct: Acct = Acct.UNKNOWN,
|
||||
// acctのハッシュ値
|
||||
var acctHash: String = "",
|
||||
// アプリサーバから受け取ったハッシュ
|
||||
|
@ -100,7 +100,7 @@ class AccountNotificationStatus(
|
|||
cursor ?: error("cursor is null!")
|
||||
AccountNotificationStatus(
|
||||
id = cursor.getLong(idxId),
|
||||
acct = cursor.getString(idxAcct),
|
||||
acct = Acct.parse(cursor.getString(idxAcct)),
|
||||
acctHash = cursor.getString(idxAcctHash),
|
||||
appServerHash = cursor.getStringOrNull(idxAppServerHash),
|
||||
pushKeyPrivate = cursor.getBlobOrNull(idxPushKeyPrivate),
|
||||
|
@ -119,7 +119,7 @@ class AccountNotificationStatus(
|
|||
|
||||
// ID以外のカラムをContentValuesに変換する
|
||||
fun toContentValues() = ContentValues().apply {
|
||||
put(COL_ACCT, acct)
|
||||
put(COL_ACCT, acct.ascii)
|
||||
put(COL_ACCT_HASH, acctHash)
|
||||
put(COL_APP_SERVER_HASH, appServerHash)
|
||||
put(COL_PUSH_KEY_PRIVATE, pushKeyPrivate)
|
||||
|
@ -154,7 +154,7 @@ class AccountNotificationStatus(
|
|||
|
||||
private fun newInstance(acct: Acct) =
|
||||
AccountNotificationStatus(
|
||||
acct = acct.ascii,
|
||||
acct = acct,
|
||||
acctHash = acct.ascii.encodeUTF8().digestSHA256().encodeBase64Url()
|
||||
)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.ContentValues
|
|||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.provider.BaseColumns
|
||||
import jp.juggler.subwaytooter.api.entity.Acct
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
import jp.juggler.util.data.MetaColumns
|
||||
import jp.juggler.util.data.TableCompanion
|
||||
import jp.juggler.util.data.replaceTo
|
||||
|
@ -15,7 +16,6 @@ class NotificationShown(
|
|||
var acct: String = "",
|
||||
var notificationId: String = "",
|
||||
var timeCreate: Long = System.currentTimeMillis(),
|
||||
var timeDismiss: Long = 0L,
|
||||
) {
|
||||
companion object : TableCompanion {
|
||||
private val log = LogCategory("NotificationShown")
|
||||
|
@ -24,13 +24,11 @@ class NotificationShown(
|
|||
private const val COL_ACCT = "a"
|
||||
private const val COL_NOTIFICATION_ID = "ni"
|
||||
private const val COL_TIME_CREATE = "tc"
|
||||
private const val COL_TIME_DISMISS = "td"
|
||||
private val columnList = MetaColumns(table, initialVersion = 65).apply {
|
||||
column(0, COL_ID, MetaColumns.TS_INT_PRIMARY_KEY_NOT_NULL)
|
||||
column(0, COL_ACCT, MetaColumns.TS_EMPTY_NOT_NULL)
|
||||
column(0, COL_NOTIFICATION_ID, MetaColumns.TS_EMPTY_NOT_NULL)
|
||||
column(0, COL_TIME_CREATE, MetaColumns.TS_ZERO_NOT_NULL)
|
||||
column(0, COL_TIME_DISMISS, MetaColumns.TS_ZERO_NOT_NULL)
|
||||
createExtra = {
|
||||
arrayOf(
|
||||
"create unique index if not exists ${table}_a on $table($COL_ACCT,$COL_NOTIFICATION_ID)",
|
||||
|
@ -122,24 +120,29 @@ class NotificationShown(
|
|||
}
|
||||
}
|
||||
|
||||
fun cleayByAcct(acct: String) {
|
||||
fun cleayByAcct(acct: Acct) {
|
||||
db.execSQL(
|
||||
"delete from $table where $COL_ACCT=?",
|
||||
arrayOf(acct)
|
||||
)
|
||||
}
|
||||
|
||||
fun duplicateOrPut(acct: String, notificationId: String): Boolean {
|
||||
fun duplicateOrPut(acct: Acct, notificationId: String): Boolean {
|
||||
try {
|
||||
// 有効なIDがない場合は重複排除しない
|
||||
when (notificationId) {
|
||||
"", EntityId.DEFAULT.toString() -> return false
|
||||
}
|
||||
|
||||
db.rawQuery(
|
||||
"select $COL_ID from $table where $COL_ACCT=? and $COL_NOTIFICATION_ID=? limit 1",
|
||||
arrayOf(acct, notificationId)
|
||||
arrayOf(acct.ascii, notificationId)
|
||||
)?.use {
|
||||
if (it.count > 0) return true
|
||||
}
|
||||
ContentValues().apply {
|
||||
put(COL_TIME_CREATE, System.currentTimeMillis())
|
||||
put(COL_ACCT, acct)
|
||||
put(COL_ACCT, acct.ascii)
|
||||
put(COL_NOTIFICATION_ID, notificationId)
|
||||
}.replaceTo(db, table)
|
||||
} catch (ex: Throwable) {
|
||||
|
@ -148,5 +151,23 @@ class NotificationShown(
|
|||
return false
|
||||
}
|
||||
|
||||
fun isDuplicate(acct: Acct, notificationId: String): Boolean {
|
||||
try {
|
||||
// 有効なIDがない場合は重複排除しない
|
||||
when (notificationId) {
|
||||
"", EntityId.DEFAULT.toString() -> return false
|
||||
}
|
||||
|
||||
db.rawQuery(
|
||||
"select $COL_ID from $table where $COL_ACCT=? and $COL_NOTIFICATION_ID=? limit 1",
|
||||
arrayOf(acct.ascii, notificationId)
|
||||
)?.use {
|
||||
if (it.count > 0) return true
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "isDuplicate failed.")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ data class PushMessage(
|
|||
// DBの主ID
|
||||
var id: Long = 0L,
|
||||
// 通知を受け取るアカウントのacct。通知のタイトルでもある
|
||||
var loginAcct: String? = null,
|
||||
var loginAcct: Acct? = null,
|
||||
// 通知情報に含まれるタイムスタンプ
|
||||
var timestamp: Long = System.currentTimeMillis(),
|
||||
// 通知を受信/保存した時刻
|
||||
|
@ -151,7 +151,7 @@ data class PushMessage(
|
|||
fun readRow(cursor: Cursor) =
|
||||
PushMessage(
|
||||
id = cursor.getLong(idxId),
|
||||
loginAcct = cursor.getStringOrNull(idxLoginAcct),
|
||||
loginAcct = cursor.getStringOrNull(idxLoginAcct)?.let{Acct.parse(it)},
|
||||
timestamp = cursor.getLong(idxTimestamp),
|
||||
timeSave = cursor.getLong(idxTimeSave),
|
||||
timeDismiss = cursor.getLong(idxTimeDismiss),
|
||||
|
@ -183,7 +183,7 @@ data class PushMessage(
|
|||
|
||||
// ID以外のカラムをContentValuesに変換する
|
||||
fun toContentValues() = ContentValues().apply {
|
||||
put(COL_LOGIN_ACCT, loginAcct)
|
||||
put(COL_LOGIN_ACCT, loginAcct?.ascii)
|
||||
put(COL_TIMESTAMP, timestamp)
|
||||
put(COL_TIME_SAVE, timeSave)
|
||||
put(COL_TIME_DISMISS, timeDismiss)
|
||||
|
@ -253,6 +253,18 @@ data class PushMessage(
|
|||
db.queryAll(TABLE, "$COL_TIME_SAVE desc")
|
||||
?.use { ColIdx(it).readAll(it) }
|
||||
?: emptyList()
|
||||
|
||||
fun deleteAccount(acct: Acct) {
|
||||
try {
|
||||
db.execSQL(
|
||||
"delete from $TABLE where $COL_LOGIN_ACCT=?",
|
||||
arrayOf(acct.ascii)
|
||||
)
|
||||
fireDataChanged()
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "sweep failed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode() = if (id == 0L) super.hashCode() else id.hashCode()
|
||||
|
|
|
@ -115,9 +115,14 @@ class SavedAccount(
|
|||
@JsonPropBoolean("notificationPullEnable", false)
|
||||
var notificationPullEnable by jsonDelegates.boolean
|
||||
|
||||
@JsonPropInt("notificationAccentColor", 0)
|
||||
var notificationAccentColor by jsonDelegates.int
|
||||
|
||||
init {
|
||||
log.i("ctor acctArg $acctArg")
|
||||
|
||||
// acctArg はMastodonの生のやつで、ドメイン部分がない場合がある
|
||||
// Acct.parse はHost部分がnullのacctになるかもしれない
|
||||
val tmpAcct = Acct.parse(acctArg)
|
||||
this.username = tmpAcct.username
|
||||
if (username.isEmpty()) error("missing username in acct")
|
||||
|
@ -128,6 +133,7 @@ class SavedAccount(
|
|||
this.apiHost = tmpApiHost ?: tmpApDomain ?: tmpAcct.host ?: error("missing apiHost")
|
||||
this.apDomain = tmpApDomain ?: tmpApiHost ?: tmpAcct.host ?: error("missing apDomain")
|
||||
|
||||
// Full Acct
|
||||
this.acct = tmpAcct.followHost(apDomain)
|
||||
}
|
||||
|
||||
|
@ -590,15 +596,24 @@ class SavedAccount(
|
|||
fun loadAccountList() =
|
||||
ArrayList<SavedAccount>().also { result ->
|
||||
try {
|
||||
db.query(
|
||||
table,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).use { cursor ->
|
||||
db.rawQuery("select * from $table", emptyArray()).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
parse(lazyContext, cursor)?.let { result.add(it) }
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "loadAccountList failed.")
|
||||
lazyContext.showToast(
|
||||
true,
|
||||
ex.withCaption("(SubwayTooter) broken in-app database?")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRealAccounts() =
|
||||
ArrayList<SavedAccount>().also { result ->
|
||||
try {
|
||||
db.rawQuery("select * from $table where $COL_USER not like '?%'", emptyArray()).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
parse(lazyContext, cursor)?.let { result.add(it) }
|
||||
}
|
||||
|
@ -892,7 +907,8 @@ class SavedAccount(
|
|||
|
||||
TootNotification.TYPE_STATUS_REFERENCE -> notificationStatusReference
|
||||
|
||||
else -> false
|
||||
// 未知の通知はオフらない
|
||||
else -> true
|
||||
}
|
||||
|
||||
fun getResizeConfig() =
|
||||
|
|
|
@ -231,7 +231,7 @@ class CustomEmojiCache(
|
|||
data = try {
|
||||
App1.getHttpCached(request.url)
|
||||
} catch (ex: Throwable) {
|
||||
log.w(ex, "get failed. url=${request.url}")
|
||||
log.w( "get failed. url=${request.url}")
|
||||
null
|
||||
}
|
||||
te = elapsedTime
|
||||
|
|
|
@ -224,15 +224,7 @@ class CustomEmojiLister(
|
|||
builder.post(JsonObject().toRequestBody())
|
||||
}?.decodeJsonObject()
|
||||
?.jsonArray("emojis")
|
||||
?.let { emojis12 ->
|
||||
parseList(emojis12) {
|
||||
CustomEmoji.decodeMisskey(
|
||||
accessInfo.apDomain,
|
||||
accessInfo.apiHost,
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
?.let { parseList(it, CustomEmoji::decodeMisskey) }
|
||||
|
||||
// v13のemojisを読む
|
||||
suspend fun misskeyEmojis13(): List<CustomEmoji>? =
|
||||
|
@ -247,11 +239,7 @@ class CustomEmojiLister(
|
|||
?.jsonArray("emojis")
|
||||
?.let { emojis13 ->
|
||||
parseList(emojis13) {
|
||||
CustomEmoji.decodeMisskey13(
|
||||
accessInfo.apDomain,
|
||||
accessInfo.apiHost,
|
||||
it
|
||||
)
|
||||
CustomEmoji.decodeMisskey13(accessInfo.apiHost, it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,13 +249,7 @@ class CustomEmojiLister(
|
|||
"https://$cacheKey/api/v1/custom_emojis",
|
||||
accessInfo = accessInfo
|
||||
)?.let { data ->
|
||||
parseList(data.decodeJsonArray()) {
|
||||
CustomEmoji.decode(
|
||||
accessInfo.apDomain,
|
||||
accessInfo.apiHost,
|
||||
it
|
||||
)
|
||||
}
|
||||
parseList(data.decodeJsonArray(), CustomEmoji::decodeMastodon)
|
||||
}
|
||||
|
||||
val list = when {
|
||||
|
|
|
@ -20,8 +20,8 @@ class DecodeOptions(
|
|||
var decodeEmoji: Boolean = false,
|
||||
var attachmentList: ArrayList<TootAttachmentLike>? = null,
|
||||
var linkTag: Any? = null,
|
||||
var emojiMapCustom: HashMap<String, CustomEmoji>? = null,
|
||||
var emojiMapProfile: HashMap<String, NicoProfileEmoji>? = null,
|
||||
var emojiMapCustom: Map<String, CustomEmoji>? = null,
|
||||
var emojiMapProfile: Map<String, NicoProfileEmoji>? = null,
|
||||
var highlightTrie: WordTrieTree? = null,
|
||||
var unwrapEmojiImageTag: Boolean = false,
|
||||
var enlargeCustomEmoji: Float = 1f,
|
||||
|
|
|
@ -469,6 +469,56 @@ object EmojiDecoder {
|
|||
val useEmojioneShortcode = PrefB.bpEmojioneShortcode.value
|
||||
val disableEmojiAnimation = PrefB.bpDisableEmojiAnimation.value
|
||||
|
||||
// カスタム絵文字のアニメーション切り替え
|
||||
fun CustomEmoji.customEmojiToUrl(): String = when {
|
||||
disableEmojiAnimation && staticUrl?.isNotEmpty() == true -> staticUrl
|
||||
else -> this.url
|
||||
}
|
||||
|
||||
fun findEmojiMisskey13(name: String): String? {
|
||||
val cols = name.split("@", limit = 2)
|
||||
val apiHostAscii = options.linkHelper?.apiHost?.ascii
|
||||
|
||||
// @以降にあるホスト名か、投稿者のホスト名か、閲覧先サーバのホスト名
|
||||
val userHost = cols.elementAtOrNull(1)
|
||||
?: options.authorDomain?.apiHost?.ascii
|
||||
?: apiHostAscii
|
||||
log.i(
|
||||
"decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${
|
||||
cols.elementAtOrNull(1)
|
||||
} apiHostAscii=$apiHostAscii, userHost=$userHost"
|
||||
)
|
||||
|
||||
when {
|
||||
// 絵文字プロクシを利用できない
|
||||
apiHostAscii == null -> {
|
||||
log.w("decodeEmoji Misskey13 missing apiHostAscii")
|
||||
}
|
||||
userHost != null && userHost != "." && userHost != apiHostAscii -> {
|
||||
// 投稿者のホスト名を使う
|
||||
return "https://$apiHostAscii/emoji/${
|
||||
cols.elementAtOrNull(0)
|
||||
}@$userHost.webp"
|
||||
}
|
||||
else -> {
|
||||
// 存在確認せずに絵文字プロキシのURLを返す
|
||||
// 閲覧先サーバの絵文字を探す
|
||||
App1.custom_emoji_lister.getCachedEmoji(apiHostAscii, name)
|
||||
?.let { return it.customEmojiToUrl() }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
fun findCustomEmojiUrl(name: String): String? {
|
||||
emojiMapCustom?.get(name)?.customEmojiToUrl()
|
||||
?.let{ return it}
|
||||
val misskeyVersion = options.linkHelper?.misskeyVersion ?: 0
|
||||
return if (misskeyVersion >= 13) {
|
||||
findEmojiMisskey13(name = name)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
splitShortCode(s, callback = object : ShortCodeSplitterCallback {
|
||||
override fun onString(part: String) {
|
||||
builder.addUnicodeString(part)
|
||||
|
@ -487,53 +537,7 @@ object EmojiDecoder {
|
|||
}
|
||||
}
|
||||
|
||||
// カスタム絵文字
|
||||
fun CustomEmoji.customEmojiToUrl(): String = when {
|
||||
disableEmojiAnimation && staticUrl?.isNotEmpty() == true ->
|
||||
this.staticUrl
|
||||
else ->
|
||||
this.url
|
||||
}
|
||||
|
||||
fun findCustomEmojiUrl(): String? {
|
||||
val misskeyVersion = options.linkHelper?.misskeyVersion ?: 0
|
||||
if (misskeyVersion >= 13) {
|
||||
val cols = name.split("@", limit = 2)
|
||||
val apiHostAscii = options.linkHelper?.apiHost?.ascii
|
||||
|
||||
// @以降にあるホスト名か、投稿者のホスト名か、閲覧先サーバのホスト名
|
||||
val userHost = cols.elementAtOrNull(1)
|
||||
?: options.authorDomain?.apiHost?.ascii
|
||||
?: apiHostAscii
|
||||
|
||||
log.i(
|
||||
"decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${
|
||||
cols.elementAtOrNull(1)
|
||||
} apiHostAscii=$apiHostAscii, userHost=$userHost"
|
||||
)
|
||||
|
||||
when {
|
||||
// 絵文字プロクシを利用できない
|
||||
apiHostAscii == null -> {
|
||||
log.w("decodeEmoji Misskey13 missing apiHostAscii")
|
||||
}
|
||||
userHost != null && userHost != "." && userHost != apiHostAscii -> {
|
||||
// 投稿者のホスト名を使う
|
||||
return "https://$apiHostAscii/emoji/${
|
||||
cols.elementAtOrNull(0)
|
||||
}@$userHost.webp"
|
||||
}
|
||||
else -> {
|
||||
// 閲覧先サーバの絵文字を探す
|
||||
App1.custom_emoji_lister.getCachedEmoji(apiHostAscii, name)
|
||||
?.let { return it.customEmojiToUrl() }
|
||||
}
|
||||
}
|
||||
}
|
||||
return emojiMapCustom?.get(name)?.customEmojiToUrl()
|
||||
}
|
||||
|
||||
val url = findCustomEmojiUrl()
|
||||
val url = findCustomEmojiUrl(name)
|
||||
if (url != null) {
|
||||
builder.addNetworkEmojiSpan(part, url)
|
||||
return
|
||||
|
|
|
@ -15,23 +15,27 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoader.LoadData
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable
|
||||
import com.bumptech.glide.load.resource.gif.MyGifDrawable
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.ImageViewTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import jp.juggler.subwaytooter.pref.PrefB
|
||||
import jp.juggler.util.data.clip
|
||||
import jp.juggler.util.data.notEmpty
|
||||
import jp.juggler.util.log.LogCategory
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class MyNetworkImageView : AppCompatImageView {
|
||||
|
||||
companion object {
|
||||
internal val log = LogCategory("MyNetworkImageView")
|
||||
}
|
||||
|
||||
// ロード中などに表示するDrawableのリソースID
|
||||
private var mDefaultImage: Drawable? = null
|
||||
|
||||
|
@ -43,7 +47,7 @@ class MyNetworkImageView : AppCompatImageView {
|
|||
|
||||
// 表示したい画像のURL
|
||||
private var mUrl: String? = null
|
||||
private var mMayGif: Boolean = false
|
||||
private var mMayAnime: Boolean = false
|
||||
|
||||
// 非同期処理のキャンセル
|
||||
private var mTarget: Target<*>? = null
|
||||
|
@ -76,20 +80,19 @@ class MyNetworkImageView : AppCompatImageView {
|
|||
fun setImageUrl(
|
||||
r: Float,
|
||||
url: String?,
|
||||
gifUrlArg: String? = null,
|
||||
animeUrl: String? = null,
|
||||
) {
|
||||
|
||||
mCornerRadius = r
|
||||
|
||||
val gifUrl = if (PrefB.bpEnableGifAnimation.value) gifUrlArg else null
|
||||
|
||||
if (gifUrl?.isNotEmpty() == true) {
|
||||
mUrl = gifUrl
|
||||
mMayGif = true
|
||||
} else {
|
||||
mUrl = url
|
||||
mMayGif = false
|
||||
if (PrefB.bpEnableGifAnimation.value) {
|
||||
animeUrl?.notEmpty()?.let {
|
||||
mUrl = it
|
||||
mMayAnime = true
|
||||
loadImageIfNecessary()
|
||||
return
|
||||
}
|
||||
}
|
||||
mUrl = url
|
||||
mMayAnime = false
|
||||
loadImageIfNecessary()
|
||||
}
|
||||
|
||||
|
@ -178,13 +181,15 @@ class MyNetworkImageView : AppCompatImageView {
|
|||
|
||||
val glideUrl = GlideUrl(url, glideHeaders)
|
||||
|
||||
mTarget = if (mMayGif) {
|
||||
mTarget = if (mMayAnime) {
|
||||
getGlide()
|
||||
?.load(glideUrl)
|
||||
?.listener(listener)
|
||||
?.into(MyTargetGif(url))
|
||||
} else {
|
||||
getGlide()
|
||||
?.load(glideUrl)
|
||||
?.listener(listener)
|
||||
?.into(MyTarget(url))
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
|
@ -563,4 +568,61 @@ class MyNetworkImageView : AppCompatImageView {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = LogCategory("MyNetworkImageView")
|
||||
private val listener = MyRequestListener()
|
||||
private val misskey13ModelLoader = Misskey13ModelLoader()
|
||||
}
|
||||
|
||||
class MyRequestListener : RequestListener<Drawable> {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
return false // Allow calling onResourceReady on the Target.
|
||||
}
|
||||
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
e?.let {
|
||||
log.e(it, "onLoadFailed")
|
||||
it.rootCauses?.forEach { cause ->
|
||||
val message = cause?.message
|
||||
when {
|
||||
cause == null -> Unit
|
||||
message?.contains("setDataSource failed: status") == true ||
|
||||
message?.contains("etDataSourceCallback failed: status") == true
|
||||
-> log.w(message)
|
||||
else -> log.e(cause, "caused by")
|
||||
}
|
||||
}
|
||||
}
|
||||
return false // Allow calling onLoadFailed on the Target.
|
||||
}
|
||||
}
|
||||
|
||||
class Misskey13ModelLoader : ModelLoader<String?, ByteBuffer> {
|
||||
override fun handles(model: String): Boolean {
|
||||
return model.startsWith("http") &&
|
||||
model.contains(".webp?")
|
||||
}
|
||||
|
||||
override fun buildLoadData(
|
||||
model: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options,
|
||||
): LoadData<ByteBuffer>? {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#ADADAD"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19v-4.58l0.99,0.99 4,-4 4,4 4,-3.99L19,12.43L19,19zM19,9.59l-1.01,-1.01 -4,4.01 -4,-4 -4,4 -0.99,-1L5,5h14v4.59z"/>
|
||||
</vector>
|
|
@ -594,6 +594,42 @@
|
|||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:id="@+id/tvNotificationAccentColor"
|
||||
android:text="@string/notification_accent_color" />
|
||||
|
||||
<LinearLayout
|
||||
style="@style/setting_row_form"
|
||||
android:id="@+id/llNotificationAccentColor"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnNotificationAccentColorEdit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/edit"
|
||||
android:textAllCaps="false"
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnNotificationAccentColorReset"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/reset"
|
||||
android:textAllCaps="false"
|
||||
/>
|
||||
|
||||
<View android:id="@+id/vNotificationAccentColorColor"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="8dp"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvPushPolicyDesc"
|
||||
style="@style/setting_row_label"
|
||||
|
@ -631,7 +667,7 @@
|
|||
|
||||
<TextView
|
||||
style="@style/setting_row_label"
|
||||
android:text="@string/push_notification_use" />
|
||||
android:text="@string/pull_notification_use" />
|
||||
|
||||
<LinearLayout style="@style/setting_row_form">
|
||||
|
||||
|
|
|
@ -1237,4 +1237,6 @@
|
|||
<string name="pull_notification_use">定期的なプル通知チェックを使う</string>
|
||||
<string name="push_subscription_exists_updateing">現在のプッシュ購読を更新しています…</string>
|
||||
<string name="manually_update">手動Manually update</string>
|
||||
<string name="notification_push_distributor_disabled">通知のプッシュ配送サービスが選択されていません</string>
|
||||
<string name="notification_accent_color">通知のアクセント色</string>
|
||||
</resources>
|
||||
|
|
|
@ -166,7 +166,8 @@
|
|||
<color name="colorNotificationAccentReaction">#f5f233</color>
|
||||
<color name="colorNotificationAccentReblog">#39e3d5</color>
|
||||
<color name="colorNotificationAccentReply">#ff3dbb</color>
|
||||
<color name="colorNotificationAccentSignUp">#f56a33</color>
|
||||
<color name="colorNotificationAccentAdminSignUp">#f56a33</color>
|
||||
<color name="colorNotificationAccentAdminReport">#ff0000</color>
|
||||
<color name="colorNotificationAccentStatus">#33f597</color>
|
||||
<color name="colorNotificationAccentUnfollow">#9433f5</color>
|
||||
<color name="colorNotificationAccentUnknown">#ae1aed</color>
|
||||
|
|
|
@ -1253,4 +1253,6 @@
|
|||
<string name="fedibird_capacities">Fedibird capacities</string>
|
||||
<string name="push_subscription_exists_updateing">Updating current push subscription…</string>
|
||||
<string name="manually_update">Manually update</string>
|
||||
<string name="notification_push_distributor_disabled">Notification push distributor not selected.</string>
|
||||
<string name="notification_accent_color">Notification accent color</string>
|
||||
</resources>
|
||||
|
|
|
@ -101,6 +101,7 @@ dependencies {
|
|||
// api "io.insert-koin:koin-androidx-navigation:$koinVersion"
|
||||
// api "io.insert-koin:koin-androidx-compose:$koinVersion"
|
||||
|
||||
api "com.github.zjupure:webpdecoder:2.3.$glideVersion"
|
||||
api "com.github.bumptech.glide:glide:$glideVersion"
|
||||
api "com.github.bumptech.glide:annotations:$glideVersion"
|
||||
api("com.github.bumptech.glide:okhttp3-integration:$glideVersion") {
|
||||
|
|
Loading…
Reference in New Issue