Misskeyでプッシュ通知の動作確認を行った。
This commit is contained in:
parent
ecbed39f5b
commit
995e7a7504
|
@ -46,6 +46,5 @@ dependencies {
|
||||||
api project(":apng")
|
api project(":apng")
|
||||||
implementation project(":base")
|
implementation project(":base")
|
||||||
|
|
||||||
implementation "com.github.zjupure:webpdecoder:2.3.$glideVersion"
|
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,3 +21,7 @@
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
-dontobfuscate
|
-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 {
|
lintOptions {
|
||||||
disable "MissingTranslation"
|
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 {
|
debug {
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,10 @@
|
||||||
kotlinx.serialization.KSerializer serializer(...);
|
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 everything
|
||||||
-keep class ** { *; }
|
-keep class ** { *; }
|
||||||
-keepclassmembers class ** { *** Companion; }
|
-keepclassmembers class ** { *** Companion; }
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package jp.juggler.subwaytooter.api
|
package jp.juggler.subwaytooter.api
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.*
|
||||||
|
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.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.*
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
@ -36,7 +40,7 @@ class TestDuplicateMap {
|
||||||
if (url != null) put("url", url)
|
if (url != null) put("url", url)
|
||||||
}
|
}
|
||||||
|
|
||||||
return TootStatus(parser, itemJson)
|
return tootStatus(parser, itemJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun testDuplicateStatus(): ArrayList<TimelineItem> {
|
private fun testDuplicateStatus(): ArrayList<TimelineItem> {
|
||||||
|
@ -63,7 +67,7 @@ class TestDuplicateMap {
|
||||||
put("url", "http://${parser.apiHost}/@user1")
|
put("url", "http://${parser.apiHost}/@user1")
|
||||||
}
|
}
|
||||||
|
|
||||||
val account1 = TootAccount(parser, account1Json)
|
val account1 = tootAccount(parser, account1Json)
|
||||||
assertNotNull(account1)
|
assertNotNull(account1)
|
||||||
|
|
||||||
val map = DuplicateMap()
|
val map = DuplicateMap()
|
||||||
|
@ -122,7 +126,7 @@ class TestDuplicateMap {
|
||||||
put("id", id)
|
put("id", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
val item = TootNotification(parser, itemJson)
|
val item = tootNotification(parser, itemJson)
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
generatedItems.add(item)
|
generatedItems.add(item)
|
||||||
assertEquals(false, map.isDuplicate(item))
|
assertEquals(false, map.isDuplicate(item))
|
||||||
|
@ -178,7 +182,7 @@ class TestDuplicateMap {
|
||||||
put("url", "http://${parser.apiHost}/@user$id")
|
put("url", "http://${parser.apiHost}/@user$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
val item = TootAccountRef.notNull(parser, TootAccount(parser, itemJson))
|
val item = tootAccountRef(parser, tootAccount(parser, itemJson))
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
generatedItems.add(item)
|
generatedItems.add(item)
|
||||||
assertEquals(false, map.isDuplicate(item))
|
assertEquals(false, map.isDuplicate(item))
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package jp.juggler.subwaytooter.api.entity
|
package jp.juggler.subwaytooter.api.entity
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.*
|
||||||
|
@ -30,42 +30,42 @@ class TestEntityUtils {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseItem() {
|
fun testParseItem() {
|
||||||
assertEquals(null, parseItem(::TestEntity, null))
|
assertEquals(null, parseItem(null, ::TestEntity))
|
||||||
|
|
||||||
run {
|
run {
|
||||||
val src = """{"s":null,"l":"100"}""".decodeJsonObject()
|
val src = """{"s":null,"l":"100"}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, src)
|
val item = parseItem(src, ::TestEntity)
|
||||||
assertNull(item)
|
assertNull(item)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"","l":"100"}""".decodeJsonObject()
|
val src = """{"s":"","l":"100"}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, src)
|
val item = parseItem(src, ::TestEntity)
|
||||||
assertNull(item)
|
assertNull(item)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"A","l":null}""".decodeJsonObject()
|
val src = """{"s":"A","l":null}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, src)
|
val item = parseItem(src, ::TestEntity)
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
assertEquals(src.optString("s"), item?.s)
|
assertEquals(src.optString("s"), item?.s)
|
||||||
assertEquals(src.optLong("l"), item?.l)
|
assertEquals(src.optLong("l"), item?.l)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"A","l":""}""".decodeJsonObject()
|
val src = """{"s":"A","l":""}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, src)
|
val item = parseItem(src, ::TestEntity)
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
assertEquals(src.optString("s"), item?.s)
|
assertEquals(src.optString("s"), item?.s)
|
||||||
assertEquals(src.optLong("l"), item?.l)
|
assertEquals(src.optLong("l"), item?.l)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"A","l":100}""".decodeJsonObject()
|
val src = """{"s":"A","l":100}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, src)
|
val item = parseItem(src, ::TestEntity)
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
assertEquals(src.optString("s"), item?.s)
|
assertEquals(src.optString("s"), item?.s)
|
||||||
assertEquals(src.optLong("l"), item?.l)
|
assertEquals(src.optLong("l"), item?.l)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"A","l":"100"}""".decodeJsonObject()
|
val src = """{"s":"A","l":"100"}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, src)
|
val item = parseItem(src, ::TestEntity)
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
assertEquals(src.optString("s"), item?.s)
|
assertEquals(src.optString("s"), item?.s)
|
||||||
assertEquals(src.optLong("l"), item?.l)
|
assertEquals(src.optLong("l"), item?.l)
|
||||||
|
@ -74,74 +74,74 @@ class TestEntityUtils {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseList() {
|
fun testParseList() {
|
||||||
assertEquals(0, parseList(::TestEntity, null).size)
|
assertEquals(0, parseList(null, ::TestEntity).size)
|
||||||
|
|
||||||
val src = JsonArray()
|
val src = JsonArray()
|
||||||
assertEquals(0, parseList(::TestEntity, src).size)
|
assertEquals(0, parseList(src, ::TestEntity).size)
|
||||||
|
|
||||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
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())
|
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseList(::TestEntity, src).size)
|
assertEquals(2, parseList(src, ::TestEntity).size)
|
||||||
|
|
||||||
// error
|
// error
|
||||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseList(::TestEntity, src).size)
|
assertEquals(2, parseList(src, ::TestEntity).size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseListOrNull() {
|
fun testParseListOrNull() {
|
||||||
assertEquals(null, parseListOrNull(::TestEntity, null))
|
assertEquals(null, parseListOrNull(null, ::TestEntity))
|
||||||
|
|
||||||
val src = JsonArray()
|
val src = JsonArray()
|
||||||
assertEquals(null, parseListOrNull(::TestEntity, src))
|
assertEquals(null, parseListOrNull(src, ::TestEntity))
|
||||||
|
|
||||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
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())
|
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseListOrNull(::TestEntity, src)?.size)
|
assertEquals(2, parseListOrNull(src, ::TestEntity)?.size)
|
||||||
|
|
||||||
// error
|
// error
|
||||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseListOrNull(::TestEntity, src)?.size)
|
assertEquals(2, parseListOrNull(src, ::TestEntity)?.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseMap() {
|
fun testParseMap() {
|
||||||
assertEquals(0, parseMap(::TestEntity, null).size)
|
assertEquals(0, parseMap(null, ::TestEntity).size)
|
||||||
|
|
||||||
val src = JsonArray()
|
val src = JsonArray()
|
||||||
assertEquals(0, parseMap(::TestEntity, src).size)
|
assertEquals(0, parseMap(null, ::TestEntity).size)
|
||||||
|
|
||||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
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())
|
src.add("""{"s":"B","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseMap(::TestEntity, src).size)
|
assertEquals(2, parseMap(src, ::TestEntity).size)
|
||||||
|
|
||||||
// error
|
// error
|
||||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseMap(::TestEntity, src).size)
|
assertEquals(2, parseMap(src, ::TestEntity).size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseMapOrNull() {
|
fun testParseMapOrNull() {
|
||||||
assertEquals(null, parseMapOrNull(::TestEntity, null))
|
assertEquals(null, parseMapOrNull(null, ::TestEntity))
|
||||||
|
|
||||||
val src = JsonArray()
|
val src = JsonArray()
|
||||||
assertEquals(null, parseMapOrNull(::TestEntity, src))
|
assertEquals(null, parseMapOrNull(src, ::TestEntity))
|
||||||
|
|
||||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
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())
|
src.add("""{"s":"B","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseMapOrNull(::TestEntity, src)?.size)
|
assertEquals(2, parseMapOrNull(src, ::TestEntity)?.size)
|
||||||
|
|
||||||
// error
|
// error
|
||||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseMapOrNull(::TestEntity, src)?.size)
|
assertEquals(2, parseMapOrNull(src, ::TestEntity)?.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val parser by lazy {
|
private val parser by lazy {
|
||||||
|
@ -154,42 +154,42 @@ class TestEntityUtils {
|
||||||
@Test
|
@Test
|
||||||
fun testParseItemWithParser() {
|
fun testParseItemWithParser() {
|
||||||
|
|
||||||
assertEquals(null, parseItem(::TestEntity, parser, null))
|
assertEquals(null, parseItem(null) { TestEntity(parser, it) })
|
||||||
|
|
||||||
run {
|
run {
|
||||||
val src = """{"s":null,"l":"100"}""".decodeJsonObject()
|
val src = """{"s":null,"l":"100"}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, parser, src)
|
val item = parseItem(src) { TestEntity(parser, it) }
|
||||||
assertNull(item)
|
assertNull(item)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"","l":"100"}""".decodeJsonObject()
|
val src = """{"s":"","l":"100"}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, parser, src)
|
val item = parseItem(src) { TestEntity(parser, it) }
|
||||||
assertNull(item)
|
assertNull(item)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"A","l":null}""".decodeJsonObject()
|
val src = """{"s":"A","l":null}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, parser, src)
|
val item = parseItem(src) { TestEntity(parser, it) }
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
assertEquals(src.optString("s"), item?.s)
|
assertEquals(src.optString("s"), item?.s)
|
||||||
assertEquals(src.optLong("l"), item?.l)
|
assertEquals(src.optLong("l"), item?.l)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"A","l":""}""".decodeJsonObject()
|
val src = """{"s":"A","l":""}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, parser, src)
|
val item = parseItem(src) { TestEntity(parser, it) }
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
assertEquals(src.optString("s"), item?.s)
|
assertEquals(src.optString("s"), item?.s)
|
||||||
assertEquals(src.optLong("l"), item?.l)
|
assertEquals(src.optLong("l"), item?.l)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"A","l":100}""".decodeJsonObject()
|
val src = """{"s":"A","l":100}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, parser, src)
|
val item = parseItem(src) { TestEntity(parser, it) }
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
assertEquals(src.optString("s"), item?.s)
|
assertEquals(src.optString("s"), item?.s)
|
||||||
assertEquals(src.optLong("l"), item?.l)
|
assertEquals(src.optLong("l"), item?.l)
|
||||||
}
|
}
|
||||||
run {
|
run {
|
||||||
val src = """{"s":"A","l":"100"}""".decodeJsonObject()
|
val src = """{"s":"A","l":"100"}""".decodeJsonObject()
|
||||||
val item = parseItem(::TestEntity, parser, src)
|
val item = parseItem(src) { TestEntity(parser, it) }
|
||||||
assertNotNull(item)
|
assertNotNull(item)
|
||||||
assertEquals(src.optString("s"), item?.s)
|
assertEquals(src.optString("s"), item?.s)
|
||||||
assertEquals(src.optLong("l"), item?.l)
|
assertEquals(src.optLong("l"), item?.l)
|
||||||
|
@ -198,38 +198,38 @@ class TestEntityUtils {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseListWithParser() {
|
fun testParseListWithParser() {
|
||||||
assertEquals(0, parseList(::TestEntity, parser, null).size)
|
assertEquals(0, parseList(null) { TestEntity(parser, it) }.size)
|
||||||
|
|
||||||
val src = JsonArray()
|
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())
|
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())
|
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseList(::TestEntity, parser, src).size)
|
assertEquals(2, parseList(src) { TestEntity(parser, it) }.size)
|
||||||
|
|
||||||
// error
|
// error
|
||||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseList(::TestEntity, parser, src).size)
|
assertEquals(2, parseList(src) { TestEntity(parser, it) }.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testParseListOrNullWithParser() {
|
fun testParseListOrNullWithParser() {
|
||||||
assertEquals(null, parseListOrNull(::TestEntity, parser, null))
|
assertEquals(null, parseListOrNull(null) { TestEntity(parser, it) })
|
||||||
|
|
||||||
val src = JsonArray()
|
val src = JsonArray()
|
||||||
assertEquals(null, parseListOrNull(::TestEntity, parser, src))
|
assertEquals(null, parseListOrNull(src) { TestEntity(parser, it) })
|
||||||
|
|
||||||
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
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())
|
src.add("""{"s":"A","l":"100"}""".decodeJsonObject())
|
||||||
assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size)
|
assertEquals(2, parseListOrNull(src) { TestEntity(parser, it) }?.size)
|
||||||
|
|
||||||
// error
|
// error
|
||||||
src.add("""{"s":"","l":"100"}""".decodeJsonObject())
|
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)
|
@Test(expected = RuntimeException::class)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
|
@ -16,12 +17,16 @@ import android.view.View
|
||||||
import android.widget.*
|
import android.widget.*
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.*
|
||||||
import jp.juggler.subwaytooter.api.auth.AuthBase
|
import jp.juggler.subwaytooter.api.auth.AuthBase
|
||||||
import jp.juggler.subwaytooter.api.auth.authRepo
|
import jp.juggler.subwaytooter.api.auth.authRepo
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
import jp.juggler.subwaytooter.api.entity.*
|
||||||
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
|
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
|
||||||
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
|
import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding
|
||||||
|
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||||
import jp.juggler.subwaytooter.notification.*
|
import jp.juggler.subwaytooter.notification.*
|
||||||
import jp.juggler.subwaytooter.push.PushBase
|
import jp.juggler.subwaytooter.push.PushBase
|
||||||
|
@ -64,6 +69,7 @@ import kotlin.math.max
|
||||||
|
|
||||||
class ActAccountSetting : AppCompatActivity(),
|
class ActAccountSetting : AppCompatActivity(),
|
||||||
View.OnClickListener,
|
View.OnClickListener,
|
||||||
|
ColorPickerDialogListener,
|
||||||
CompoundButton.OnCheckedChangeListener,
|
CompoundButton.OnCheckedChangeListener,
|
||||||
AdapterView.OnItemSelectedListener {
|
AdapterView.OnItemSelectedListener {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -84,6 +90,8 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
|
|
||||||
private const val ACTIVITY_STATE = "MyActivityState"
|
private const val ACTIVITY_STATE = "MyActivityState"
|
||||||
|
|
||||||
|
private const val COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR = 1
|
||||||
|
|
||||||
fun createIntent(activity: Activity, ai: SavedAccount) =
|
fun createIntent(activity: Activity, ai: SavedAccount) =
|
||||||
Intent(activity, ActAccountSetting::class.java).apply {
|
Intent(activity, ActAccountSetting::class.java).apply {
|
||||||
putExtra(KEY_ACCOUNT_DB_ID, ai.db_id)
|
putExtra(KEY_ACCOUNT_DB_ID, ai.db_id)
|
||||||
|
@ -510,6 +518,8 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
spResizeImage,
|
spResizeImage,
|
||||||
swNotificationPullEnabled,
|
swNotificationPullEnabled,
|
||||||
swNotificationPushEnabled,
|
swNotificationPushEnabled,
|
||||||
|
btnNotificationAccentColorEdit,
|
||||||
|
btnNotificationAccentColorReset,
|
||||||
).forEach { it.isEnabledAlpha = enabled }
|
).forEach { it.isEnabledAlpha = enabled }
|
||||||
|
|
||||||
// arrayOf(
|
// arrayOf(
|
||||||
|
@ -521,6 +531,7 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
showVisibility()
|
showVisibility()
|
||||||
showAcctColor()
|
showAcctColor()
|
||||||
showPushSetting()
|
showPushSetting()
|
||||||
|
showNotificationColor()
|
||||||
} finally {
|
} finally {
|
||||||
loadingBusy = false
|
loadingBusy = false
|
||||||
}
|
}
|
||||||
|
@ -547,6 +558,8 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
tvPushActions.vg(usePush)
|
tvPushActions.vg(usePush)
|
||||||
btnPushSubscription.vg(usePush)
|
btnPushSubscription.vg(usePush)
|
||||||
btnPushSubscriptionNotForce.vg(usePush)
|
btnPushSubscriptionNotForce.vg(usePush)
|
||||||
|
tvNotificationAccentColor.vg(usePush)
|
||||||
|
llNotificationAccentColor.vg(usePush)
|
||||||
}
|
}
|
||||||
|
|
||||||
run {
|
run {
|
||||||
|
@ -688,6 +701,22 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
// PullNotification.openNotificationChannelSetting(
|
// PullNotification.openNotificationChannelSetting(
|
||||||
// this
|
// 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() {
|
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 {
|
launchAndShowError {
|
||||||
|
confirm(getString(R.string.confirm_account_remove), title = getString(R.string.confirm))
|
||||||
authRepo.accountRemove(account)
|
authRepo.accountRemove(account)
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}.show()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
|
@ -1525,4 +1549,24 @@ class ActAccountSetting : AppCompatActivity(),
|
||||||
.show()
|
.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 androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import jp.juggler.subwaytooter.api.dialogOrToast
|
import jp.juggler.subwaytooter.api.dialogOrToast
|
||||||
import jp.juggler.subwaytooter.api.entity.Acct
|
|
||||||
import jp.juggler.subwaytooter.databinding.ActPushMessageListBinding
|
import jp.juggler.subwaytooter.databinding.ActPushMessageListBinding
|
||||||
import jp.juggler.subwaytooter.databinding.LvPushMessageBinding
|
import jp.juggler.subwaytooter.databinding.LvPushMessageBinding
|
||||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
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.PushMessage
|
||||||
import jp.juggler.subwaytooter.table.daoAccountNotificationStatus
|
import jp.juggler.subwaytooter.table.daoAccountNotificationStatus
|
||||||
import jp.juggler.subwaytooter.table.daoPushMessage
|
import jp.juggler.subwaytooter.table.daoPushMessage
|
||||||
|
import jp.juggler.subwaytooter.table.daoSavedAccount
|
||||||
import jp.juggler.subwaytooter.util.permissionSpecNotification
|
import jp.juggler.subwaytooter.util.permissionSpecNotification
|
||||||
import jp.juggler.subwaytooter.util.requester
|
import jp.juggler.subwaytooter.util.requester
|
||||||
import jp.juggler.util.coroutine.AppDispatchers
|
import jp.juggler.util.coroutine.AppDispatchers
|
||||||
import jp.juggler.util.coroutine.launchAndShowError
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
import jp.juggler.util.data.encodeBase64Url
|
import jp.juggler.util.data.encodeBase64Url
|
||||||
import jp.juggler.util.data.notBlank
|
import jp.juggler.util.data.notBlank
|
||||||
|
import jp.juggler.util.data.notZero
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import jp.juggler.util.os.saveToDownload
|
import jp.juggler.util.os.saveToDownload
|
||||||
import jp.juggler.util.time.formatLocalTime
|
import jp.juggler.util.time.formatLocalTime
|
||||||
|
@ -59,6 +60,9 @@ class ActPushMessageList : AppCompatActivity() {
|
||||||
private val prNotification = permissionSpecNotification.requester {
|
private val prNotification = permissionSpecNotification.requester {
|
||||||
// 特に何もしない
|
// 特に何もしない
|
||||||
}
|
}
|
||||||
|
private val acctMap by lazy {
|
||||||
|
daoSavedAccount.loadRealAccounts().associateBy { it.acct }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
prNotification.register(this)
|
prNotification.register(this)
|
||||||
|
@ -96,7 +100,7 @@ class ActPushMessageList : AppCompatActivity() {
|
||||||
launchAndShowError {
|
launchAndShowError {
|
||||||
actionsDialog {
|
actionsDialog {
|
||||||
action(getString(R.string.push_message_re_decode)) {
|
action(getString(R.string.push_message_re_decode)) {
|
||||||
pushRepo.reDecode(pm)
|
pushRepo.reprocess(pm)
|
||||||
}
|
}
|
||||||
action(getString(R.string.push_message_save_to_download_folder)) {
|
action(getString(R.string.push_message_save_to_download_folder)) {
|
||||||
export(pm)
|
export(pm)
|
||||||
|
@ -140,7 +144,7 @@ class ActPushMessageList : AppCompatActivity() {
|
||||||
if (acct == null) {
|
if (acct == null) {
|
||||||
println("!!secret key is not exported because missing recepients acct.")
|
println("!!secret key is not exported because missing recepients acct.")
|
||||||
} else {
|
} else {
|
||||||
val status = daoAccountNotificationStatus.load(Acct.parse(acct))
|
val status = daoAccountNotificationStatus.load(acct)
|
||||||
if (status == null) {
|
if (status == null) {
|
||||||
println("!!secret key is not exported because missing status for acct $acct .")
|
println("!!secret key is not exported because missing status for acct $acct .")
|
||||||
} else {
|
} else {
|
||||||
|
@ -157,12 +161,16 @@ class ActPushMessageList : AppCompatActivity() {
|
||||||
|
|
||||||
private val tintIconMap = HashMap<String, Drawable>()
|
private val tintIconMap = HashMap<String, Drawable>()
|
||||||
|
|
||||||
fun tintIcon(ic: PushMessageIconColor) =
|
fun tintIcon(pm: PushMessage, ic: PushMessageIconColor) =
|
||||||
tintIconMap.getOrPut(ic.name) {
|
tintIconMap.getOrPut("${ic.name}-${pm.loginAcct}") {
|
||||||
val context = this
|
val context = this
|
||||||
val src = ContextCompat.getDrawable(context, ic.iconId)!!
|
val src = ContextCompat.getDrawable(context, ic.iconId)!!
|
||||||
DrawableCompat.wrap(src).also {
|
DrawableCompat.wrap(src).also { d ->
|
||||||
DrawableCompat.setTint(it, ContextCompat.getColor(context, ic.colorRes))
|
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
|
pm ?: return
|
||||||
lastItem = pm
|
lastItem = pm
|
||||||
|
|
||||||
val iconAndColor = pm.iconColor()
|
|
||||||
Glide.with(views.ivSmall)
|
Glide.with(views.ivSmall)
|
||||||
.load(pm.iconSmall)
|
.load(pm.iconSmall)
|
||||||
.error(tintIcon(iconAndColor))
|
.error(tintIcon(pm, pm.iconColor()))
|
||||||
.into(views.ivSmall)
|
.into(views.ivSmall)
|
||||||
|
|
||||||
Glide.with(views.ivLarge)
|
Glide.with(views.ivLarge)
|
||||||
|
|
|
@ -1,22 +1,29 @@
|
||||||
package jp.juggler.subwaytooter
|
package jp.juggler.subwaytooter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.PictureDrawable
|
import android.graphics.drawable.PictureDrawable
|
||||||
import androidx.annotation.Nullable
|
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.GlideBuilder
|
import com.bumptech.glide.GlideBuilder
|
||||||
import com.bumptech.glide.Registry
|
import com.bumptech.glide.Registry
|
||||||
import com.bumptech.glide.annotation.GlideModule
|
import com.bumptech.glide.annotation.GlideModule
|
||||||
|
import com.bumptech.glide.integration.webp.decoder.*
|
||||||
import com.bumptech.glide.load.Options
|
import com.bumptech.glide.load.Options
|
||||||
import com.bumptech.glide.load.ResourceDecoder
|
import com.bumptech.glide.load.ResourceDecoder
|
||||||
import com.bumptech.glide.load.engine.Resource
|
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.SimpleResource
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder
|
||||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
|
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
|
||||||
import com.bumptech.glide.module.AppGlideModule
|
import com.bumptech.glide.module.AppGlideModule
|
||||||
import com.caverock.androidsvg.SVG
|
import com.caverock.androidsvg.SVG
|
||||||
import com.caverock.androidsvg.SVGParseException
|
import com.caverock.androidsvg.SVGParseException
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.ByteBuffer
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
|
@ -73,7 +80,6 @@ class MyAppGlideModule : AppGlideModule() {
|
||||||
// Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]).
|
// Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]).
|
||||||
class SvgDrawableTranscoder : ResourceTranscoder<SVG, PictureDrawable> {
|
class SvgDrawableTranscoder : ResourceTranscoder<SVG, PictureDrawable> {
|
||||||
|
|
||||||
@Nullable
|
|
||||||
override fun transcode(
|
override fun transcode(
|
||||||
toTranscode: Resource<SVG>,
|
toTranscode: Resource<SVG>,
|
||||||
options: Options,
|
options: Options,
|
||||||
|
@ -102,6 +108,65 @@ class MyAppGlideModule : AppGlideModule() {
|
||||||
registry
|
registry
|
||||||
.register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder())
|
.register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder())
|
||||||
.append(InputStream::class.java, SVG::class.java, SvgDecoder())
|
.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) {
|
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||||
|
|
|
@ -70,7 +70,7 @@ fun ActMain.accountAdd() {
|
||||||
val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) }
|
val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) }
|
||||||
addPseudoAccount(apiHost, tootInstance)?.let { a ->
|
addPseudoAccount(apiHost, tootInstance)?.let { a ->
|
||||||
showToast(false, R.string.server_confirmed)
|
showToast(false, R.string.server_confirmed)
|
||||||
addColumn(defaultInsertPosition, a, ColumnType.LOCAL)
|
addColumn(defaultInsertPosition, a, ColumnType.LOCAL,protect=true)
|
||||||
dialogHost.dismissSafe()
|
dialogHost.dismissSafe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -383,7 +383,7 @@ fun ActMain.clickBoostBy(
|
||||||
columnType: ColumnType = ColumnType.BOOSTED_BY,
|
columnType: ColumnType = ColumnType.BOOSTED_BY,
|
||||||
) {
|
) {
|
||||||
status ?: return
|
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) {
|
fun ActMain.clickBoost(accessInfo: SavedAccount, status: TootStatus, willToast: Boolean) {
|
||||||
|
|
|
@ -164,7 +164,7 @@ fun ActMain.conversationLocal(
|
||||||
else ->
|
else ->
|
||||||
ColumnType.CONVERSATION
|
ColumnType.CONVERSATION
|
||||||
},
|
},
|
||||||
statusId,
|
params = arrayOf(statusId),
|
||||||
)
|
)
|
||||||
|
|
||||||
private val reDetailedStatusTime =
|
private val reDetailedStatusTime =
|
||||||
|
@ -501,7 +501,10 @@ fun ActMain.conversationFromTootsearch(
|
||||||
val replyId = status?.in_reply_to_id
|
val replyId = status?.in_reply_to_id
|
||||||
when {
|
when {
|
||||||
status == null -> showToast(true, result.error ?: "?")
|
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)
|
else -> conversationLocal(pos, a, replyId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,10 @@ import okhttp3.Request
|
||||||
|
|
||||||
fun ActMain.clickListTl(pos: Int, accessInfo: SavedAccount, item: TimelineItem?) {
|
fun ActMain.clickListTl(pos: Int, accessInfo: SavedAccount, item: TimelineItem?) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is TootList -> addColumn(pos, accessInfo, ColumnType.LIST_TL, item.id)
|
is TootList ->
|
||||||
is MisskeyAntenna -> addColumn(pos, accessInfo, ColumnType.MISSKEY_ANTENNA_TL, item.id)
|
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 {
|
launchAndShowError {
|
||||||
actionsDialog(item.title) {
|
actionsDialog(item.title) {
|
||||||
action(getString(R.string.list_timeline)) {
|
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)) {
|
action(getString(R.string.list_member)) {
|
||||||
addColumn(
|
addColumn(
|
||||||
|
@ -47,7 +49,7 @@ fun ActMain.clickListMoreButton(pos: Int, accessInfo: SavedAccount, item: Timeli
|
||||||
pos,
|
pos,
|
||||||
accessInfo,
|
accessInfo,
|
||||||
ColumnType.LIST_MEMBER,
|
ColumnType.LIST_MEMBER,
|
||||||
item.id
|
params = arrayOf(item.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
action(getString(R.string.rename)) {
|
action(getString(R.string.rename)) {
|
||||||
|
|
|
@ -26,12 +26,7 @@ fun ActMain.clickNotificationFrom(
|
||||||
showToast(false, R.string.misskey_account_not_supported)
|
showToast(false, R.string.misskey_account_not_supported)
|
||||||
} else {
|
} else {
|
||||||
accessInfo.getFullAcct(who).validFull()?.let {
|
accessInfo.getFullAcct(who).validFull()?.let {
|
||||||
addColumn(
|
addColumn(pos, accessInfo, ColumnType.NOTIFICATION_FROM_ACCT, params = arrayOf(it))
|
||||||
pos,
|
|
||||||
accessInfo,
|
|
||||||
ColumnType.NOTIFICATION_FROM_ACCT,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ private fun ActMain.serverProfileDirectory(
|
||||||
pos,
|
pos,
|
||||||
accessInfo,
|
accessInfo,
|
||||||
ColumnType.PROFILE_DIRECTORY,
|
ColumnType.PROFILE_DIRECTORY,
|
||||||
host
|
params = arrayOf(host)
|
||||||
)
|
)
|
||||||
|
|
||||||
// 疑似アカウントで開く
|
// 疑似アカウントで開く
|
||||||
|
@ -70,7 +70,7 @@ private fun ActMain.serverProfileDirectory(
|
||||||
pos,
|
pos,
|
||||||
ai,
|
ai,
|
||||||
ColumnType.PROFILE_DIRECTORY,
|
ColumnType.PROFILE_DIRECTORY,
|
||||||
host
|
params = arrayOf(host)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ fun ActMain.serverInformation(
|
||||||
pos,
|
pos,
|
||||||
SavedAccount.na,
|
SavedAccount.na,
|
||||||
ColumnType.INSTANCE_INFORMATION,
|
ColumnType.INSTANCE_INFORMATION,
|
||||||
host
|
params = arrayOf(host),
|
||||||
)
|
)
|
||||||
|
|
||||||
// ドメインブロック一覧から解除
|
// ドメインブロック一覧から解除
|
||||||
|
|
|
@ -607,5 +607,5 @@ fun ActMain.openStatusHistory(
|
||||||
accessInfo: SavedAccount,
|
accessInfo: SavedAccount,
|
||||||
status: TootStatus,
|
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,
|
acctAscii: String? = null,
|
||||||
) {
|
) {
|
||||||
if (acctAscii == null) {
|
if (acctAscii == null) {
|
||||||
addColumn(pos, accessInfo, ColumnType.HASHTAG, tagWithoutSharp)
|
addColumn(pos, accessInfo, ColumnType.HASHTAG, params = arrayOf(tagWithoutSharp))
|
||||||
} else {
|
} else {
|
||||||
addColumn(
|
addColumn(
|
||||||
pos,
|
pos,
|
||||||
accessInfo,
|
accessInfo,
|
||||||
ColumnType.HASHTAG_FROM_ACCT,
|
ColumnType.HASHTAG_FROM_ACCT,
|
||||||
tagWithoutSharp,
|
params = arrayOf(tagWithoutSharp, acctAscii)
|
||||||
acctAscii
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,12 +38,14 @@ fun ActMain.timeline(
|
||||||
)?.let { account ->
|
)?.let { account ->
|
||||||
when (type) {
|
when (type) {
|
||||||
ColumnType.PROFILE ->
|
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 ->
|
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,
|
pos: Int,
|
||||||
accessInfo: SavedAccount,
|
accessInfo: SavedAccount,
|
||||||
host: Host,
|
host: Host,
|
||||||
) = addColumn(pos, accessInfo, ColumnType.DOMAIN_TIMELINE, host)
|
) = addColumn(pos, accessInfo, ColumnType.DOMAIN_TIMELINE, params = arrayOf(host))
|
||||||
|
|
||||||
// 指定タンスのローカルタイムラインを開く
|
// 指定タンスのローカルタイムラインを開く
|
||||||
fun ActMain.timelineLocal(
|
fun ActMain.timelineLocal(
|
||||||
|
@ -131,7 +133,7 @@ private fun ActMain.timelineAround(
|
||||||
pos: Int,
|
pos: Int,
|
||||||
id: EntityId,
|
id: EntityId,
|
||||||
type: ColumnType,
|
type: ColumnType,
|
||||||
) = addColumn(pos, accessInfo, type, id)
|
) = addColumn(pos, accessInfo, type, params = arrayOf(id))
|
||||||
|
|
||||||
// 投稿を同期してstatusIdを調べてから指定アカウントでタイムラインを開く
|
// 投稿を同期してstatusIdを調べてから指定アカウントでタイムラインを開く
|
||||||
private fun ActMain.timelineAroundByStatus(
|
private fun ActMain.timelineAroundByStatus(
|
||||||
|
|
|
@ -573,7 +573,7 @@ private fun ActMain.userProfileFromUrlOrAcct(
|
||||||
openCustomTab(whoUrl)
|
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)
|
accountListArg = accountListNonPseudo(who.apDomain)
|
||||||
)?.let { ai ->
|
)?.let { ai ->
|
||||||
if (ai.matchHost(accessInfo)) {
|
if (ai.matchHost(accessInfo)) {
|
||||||
addColumn(pos, ai, ColumnType.PROFILE, who.id)
|
addColumn(pos, ai, ColumnType.PROFILE, params = arrayOf(who.id))
|
||||||
} else {
|
} else {
|
||||||
userProfileFromUrlOrAcct(pos, ai, accessInfo.getFullAcct(who), who.url)
|
userProfileFromUrlOrAcct(pos, ai, accessInfo.getFullAcct(who), who.url)
|
||||||
}
|
}
|
||||||
|
@ -613,7 +613,7 @@ fun ActMain.userProfileLocal(
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
accessInfo.isNA -> userProfileFromAnotherAccount(pos, accessInfo, who)
|
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,
|
indexArg: Int,
|
||||||
ai: SavedAccount,
|
ai: SavedAccount,
|
||||||
type: ColumnType,
|
type: ColumnType,
|
||||||
vararg params: Any,
|
protect: Boolean = false,
|
||||||
|
params: Array<out Any> = emptyArray(),
|
||||||
): Column {
|
): Column {
|
||||||
if (!allowColumnDuplication) {
|
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)
|
val index = addColumn(col, indexArg)
|
||||||
scrollAndLoad(index)
|
scrollAndLoad(index)
|
||||||
return col
|
return col
|
||||||
|
@ -100,16 +102,16 @@ fun ActMain.addColumn(
|
||||||
indexArg: Int,
|
indexArg: Int,
|
||||||
ai: SavedAccount,
|
ai: SavedAccount,
|
||||||
type: ColumnType,
|
type: ColumnType,
|
||||||
vararg params: Any,
|
protect: Boolean = false,
|
||||||
): Column {
|
params: Array<out Any> = emptyArray(),
|
||||||
return addColumn(
|
): Column = addColumn(
|
||||||
PrefB.bpAllowColumnDuplication.value,
|
PrefB.bpAllowColumnDuplication.value,
|
||||||
indexArg,
|
indexArg,
|
||||||
ai,
|
ai,
|
||||||
type,
|
type,
|
||||||
*params
|
protect = protect,
|
||||||
|
params = params,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
fun ActMain.removeColumn(column: Column) {
|
fun ActMain.removeColumn(column: Column) {
|
||||||
val idxColumn = appState.columnIndex(column) ?: return
|
val idxColumn = appState.columnIndex(column) ?: return
|
||||||
|
@ -342,7 +344,7 @@ fun ActMain.searchFromActivityResult(data: Intent?, columnType: ColumnType) =
|
||||||
defaultInsertPosition,
|
defaultInsertPosition,
|
||||||
SavedAccount.na,
|
SavedAccount.na,
|
||||||
columnType,
|
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()) {
|
if (daoSavedAccount.isSingleAccount()) {
|
||||||
addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS)
|
addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS, protect = true)
|
||||||
addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL)
|
addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL, protect = true)
|
||||||
addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE)
|
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.column.ColumnType
|
||||||
import jp.juggler.subwaytooter.dialog.pickAccount
|
import jp.juggler.subwaytooter.dialog.pickAccount
|
||||||
import jp.juggler.subwaytooter.pref.PrefB
|
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.PrefS
|
||||||
|
import jp.juggler.subwaytooter.pref.prefDevice
|
||||||
|
import jp.juggler.subwaytooter.push.fcmHandler
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
import jp.juggler.subwaytooter.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.table.accountListCanSeeMyReactions
|
import jp.juggler.subwaytooter.table.accountListCanSeeMyReactions
|
||||||
import jp.juggler.subwaytooter.util.VersionString
|
import jp.juggler.subwaytooter.util.VersionString
|
||||||
|
@ -404,7 +407,12 @@ class SideMenuAdapter(
|
||||||
// },
|
// },
|
||||||
|
|
||||||
Item(icon = R.drawable.ic_search, title = R.string.notestock) {
|
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(),
|
Item(),
|
||||||
|
@ -524,7 +532,8 @@ class SideMenuAdapter(
|
||||||
ItemType.IT_NOTIFICATION_PERMISSION ->
|
ItemType.IT_NOTIFICATION_PERMISSION ->
|
||||||
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
|
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
|
||||||
isAllCaps = false
|
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)
|
val drawable = createColoredDrawable(actMain, icon, iconColor, 1f)
|
||||||
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||||
drawable,
|
drawable,
|
||||||
|
@ -534,15 +543,12 @@ class SideMenuAdapter(
|
||||||
)
|
)
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
drawer.closeDrawer(GravityCompat.START)
|
drawer.closeDrawer(GravityCompat.START)
|
||||||
if (actMain.prNotification.spec.listNotGranded(actMain).isNotEmpty()) {
|
notificationActionRecommend()?.second?.invoke()
|
||||||
actMain.prNotification.openAppSetting(actMain)
|
|
||||||
} else {
|
|
||||||
filterListItems()
|
filterListItems()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getTimeZoneString(context: Context): String {
|
private fun getTimeZoneString(context: Context): String {
|
||||||
try {
|
try {
|
||||||
|
@ -581,11 +587,24 @@ class SideMenuAdapter(
|
||||||
this.notifyDataSetChanged()
|
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() {
|
fun filterListItems() {
|
||||||
list = originalList.filter {
|
list = originalList.filter {
|
||||||
when (it.itemType) {
|
when (it.itemType) {
|
||||||
ItemType.IT_NOTIFICATION_PERMISSION ->
|
ItemType.IT_NOTIFICATION_PERMISSION ->
|
||||||
actMain.prNotification.spec.listNotGranded(actMain).isNotEmpty()
|
notificationActionRecommend() != null
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import jp.juggler.subwaytooter.App1
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.api.auth.AuthBase
|
import jp.juggler.subwaytooter.api.auth.AuthBase
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
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.table.SavedAccount
|
||||||
import jp.juggler.subwaytooter.util.*
|
import jp.juggler.subwaytooter.util.*
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.*
|
||||||
|
@ -603,7 +604,7 @@ suspend fun TootApiClient.syncAccountByUrl(
|
||||||
}.toPostRequestBuilder()
|
}.toPostRequestBuilder()
|
||||||
)
|
)
|
||||||
?.apply {
|
?.apply {
|
||||||
ar = TootAccountRef.mayNull(parser, parser.account(jsonObject))
|
ar = tootAccountRefOrNull(parser, parser.account(jsonObject))
|
||||||
if (ar == null && error == null) {
|
if (ar == null && error == null) {
|
||||||
setError(context.getString(R.string.user_id_conversion_failed))
|
setError(context.getString(R.string.user_id_conversion_failed))
|
||||||
}
|
}
|
||||||
|
@ -646,7 +647,7 @@ suspend fun TootApiClient.syncAccountByAcct(
|
||||||
.toPostRequestBuilder()
|
.toPostRequestBuilder()
|
||||||
)
|
)
|
||||||
?.apply {
|
?.apply {
|
||||||
ar = TootAccountRef.mayNull(parser, parser.account(jsonObject))
|
ar = tootAccountRefOrNull(parser, parser.account(jsonObject))
|
||||||
if (ar == null && error == null) {
|
if (ar == null && error == null) {
|
||||||
setError(context.getString(R.string.user_id_conversion_failed))
|
setError(context.getString(R.string.user_id_conversion_failed))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package jp.juggler.subwaytooter.api.auth
|
package jp.juggler.subwaytooter.api.auth
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import jp.juggler.subwaytooter.api.TootApiClient
|
import jp.juggler.subwaytooter.api.TootApiClient
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
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.notification.checkNotificationImmediateAll
|
||||||
import jp.juggler.subwaytooter.pref.PrefL
|
import jp.juggler.subwaytooter.pref.PrefL
|
||||||
import jp.juggler.subwaytooter.pref.lazyContext
|
import jp.juggler.subwaytooter.pref.lazyContext
|
||||||
import jp.juggler.subwaytooter.table.AcctColor
|
import jp.juggler.subwaytooter.table.*
|
||||||
import jp.juggler.subwaytooter.table.SavedAccount
|
|
||||||
import jp.juggler.subwaytooter.table.appDatabase
|
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
|
|
||||||
val Context.authRepo
|
val Context.authRepo
|
||||||
get() = AuthRepo(
|
get() = AuthRepo(
|
||||||
context = this,
|
context = this,
|
||||||
daoAcctColor = AcctColor.Access(appDatabase),
|
database = appDatabase,
|
||||||
daoSavedAccount = SavedAccount.Access(appDatabase, lazyContext),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class AuthRepo(
|
class AuthRepo(
|
||||||
private val context: Context = lazyContext,
|
private val context: Context = lazyContext,
|
||||||
|
private val database: SQLiteDatabase = appDatabase,
|
||||||
private val daoAcctColor: AcctColor.Access =
|
private val daoAcctColor: AcctColor.Access =
|
||||||
AcctColor.Access(appDatabase),
|
AcctColor.Access(database),
|
||||||
private val daoSavedAccount: SavedAccount.Access =
|
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 {
|
companion object {
|
||||||
private val log = LogCategory("AuthRepo")
|
private val log = LogCategory("AuthRepo")
|
||||||
|
@ -69,6 +72,8 @@ class AuthRepo(
|
||||||
PrefL.lpTabletTootDefaultAccount.value = -1L
|
PrefL.lpTabletTootDefaultAccount.value = -1L
|
||||||
}
|
}
|
||||||
daoSavedAccount.delete(account.db_id)
|
daoSavedAccount.delete(account.db_id)
|
||||||
|
daoPushMessage.deleteAccount(account.acct)
|
||||||
|
daoNotificationShown.cleayByAcct(account.acct)
|
||||||
// appServerUnregister(context.applicationContextSafe, account)
|
// appServerUnregister(context.applicationContextSafe, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,6 @@ class APTag(parser: TootParser, jsonArray: JsonArray?) {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
emojiList[shortcode] = CustomEmoji(
|
emojiList[shortcode] = CustomEmoji(
|
||||||
apDomain = parser.apDomain,
|
|
||||||
shortcode = shortcode,
|
shortcode = shortcode,
|
||||||
url = iconUrl,
|
url = iconUrl,
|
||||||
staticUrl = iconUrl,
|
staticUrl = iconUrl,
|
||||||
|
|
|
@ -4,11 +4,15 @@ import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||||
import jp.juggler.util.data.JsonObject
|
import jp.juggler.util.data.JsonObject
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
|
|
||||||
class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) {
|
class MisskeyNoteUpdate(
|
||||||
companion object {
|
val noteId: EntityId,
|
||||||
private val log = LogCategory("MisskeyNoteUpdate")
|
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 {
|
enum class Type {
|
||||||
REACTION,
|
REACTION,
|
||||||
UNREACTION,
|
UNREACTION,
|
||||||
|
@ -16,53 +20,34 @@ class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) {
|
||||||
VOTED
|
VOTED
|
||||||
}
|
}
|
||||||
|
|
||||||
val noteId: EntityId
|
companion object {
|
||||||
val type: Type
|
|
||||||
var reaction: String? = null
|
|
||||||
var userId: EntityId? = null
|
|
||||||
var deletedAt: Long? = null
|
|
||||||
var choice: Int? = null
|
|
||||||
var emoji: CustomEmoji? = null
|
|
||||||
|
|
||||||
init {
|
fun misskeyNoteUpdate(src: JsonObject): MisskeyNoteUpdate {
|
||||||
noteId = EntityId.mayNull(src.string("id")) ?: error("MisskeyNoteUpdate: missing note id")
|
|
||||||
|
val noteId = EntityId.mayNull(src.string("id"))
|
||||||
|
?: error("MisskeyNoteUpdate: missing note id")
|
||||||
|
|
||||||
// root.body.body
|
// root.body.body
|
||||||
val body2 = src.jsonObject("body") ?: error("MisskeyNoteUpdate: missing body")
|
val body2 = src.jsonObject("body")
|
||||||
|
?: error("MisskeyNoteUpdate: missing body")
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"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"))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
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")
|
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.R
|
||||||
import jp.juggler.subwaytooter.api.MisskeyAccountDetailMap
|
import jp.juggler.subwaytooter.api.MisskeyAccountDetailMap
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
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.api.entity.TootStatus.Companion.tootStatus
|
||||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||||
import jp.juggler.subwaytooter.pref.PrefB
|
import jp.juggler.subwaytooter.pref.PrefB
|
||||||
|
@ -386,6 +387,8 @@ open class TootAccount(
|
||||||
"""\Ahttps://($reHostIdn)/users/(\w|\w+[\w-]*\w)(?=\z|[?#])"""
|
"""\Ahttps://($reHostIdn)/users/(\w|\w+[\w-]*\w)(?=\z|[?#])"""
|
||||||
.asciiPattern()
|
.asciiPattern()
|
||||||
|
|
||||||
|
private val reMisskeyIoProxy = """\Ahttps://misskey\.io/proxy/""".toRegex()
|
||||||
|
|
||||||
fun tootAccount(parser: TootParser, src: JsonObject): TootAccount {
|
fun tootAccount(parser: TootParser, src: JsonObject): TootAccount {
|
||||||
src["_fromStream"] = parser.fromStream
|
src["_fromStream"] = parser.fromStream
|
||||||
|
|
||||||
|
@ -427,9 +430,7 @@ open class TootAccount(
|
||||||
ServiceType.MISSKEY -> {
|
ServiceType.MISSKEY -> {
|
||||||
|
|
||||||
custom_emojis =
|
custom_emojis =
|
||||||
parseMapOrNull(src.jsonArray("emojis")) {
|
parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMisskey)
|
||||||
CustomEmoji.decodeMisskey(parser.apDomain, parser.apiHost, it)
|
|
||||||
}
|
|
||||||
profile_emojis = null
|
profile_emojis = null
|
||||||
|
|
||||||
username = src.stringOrThrow("username")
|
username = src.stringOrThrow("username")
|
||||||
|
@ -476,17 +477,14 @@ open class TootAccount(
|
||||||
created_at = src.string("createdAt")
|
created_at = src.string("createdAt")
|
||||||
time_created_at = TootStatus.parseTime(created_at)
|
time_created_at = TootStatus.parseTime(created_at)
|
||||||
|
|
||||||
// https://github.com/syuilo/misskey/blob/develop/src/client/scripts/get-static-image-url.ts
|
// 画像を静止させるURLはAPIとしては提供されていない
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
avatar = src.string("avatarUrl")
|
avatar = src.string("avatarUrl")
|
||||||
avatar_static = src.string("avatarUrl")?.getStaticImageUrl()
|
avatar_static = src.string("avatarUrl")
|
||||||
header = src.string("bannerUrl")
|
header = src.string("bannerUrl")
|
||||||
header_static = src.string("bannerUrl")?.getStaticImageUrl()
|
header_static = src.string("bannerUrl")
|
||||||
|
|
||||||
pinnedNoteIds = src.stringArrayList("pinnedNoteIds")
|
pinnedNoteIds = src.stringArrayList("pinnedNoteIds")
|
||||||
if (parser.misskeyDecodeProfilePin) {
|
if (parser.misskeyDecodeProfilePin) {
|
||||||
|
@ -561,13 +559,8 @@ open class TootAccount(
|
||||||
else -> {
|
else -> {
|
||||||
|
|
||||||
// 絵文字データは先に読んでおく
|
// 絵文字データは先に読んでおく
|
||||||
custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
|
custom_emojis =
|
||||||
CustomEmoji.decode(
|
parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMastodon)
|
||||||
parser.apDomain,
|
|
||||||
parser.apiHost,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
profile_emojis = when (val o = src["profile_emojis"]) {
|
profile_emojis = when (val o = src["profile_emojis"]) {
|
||||||
is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) }
|
is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) }
|
||||||
|
@ -587,7 +580,7 @@ open class TootAccount(
|
||||||
note = src.string("note")
|
note = src.string("note")
|
||||||
|
|
||||||
source = parseSource(src.jsonObject("source"))
|
source = parseSource(src.jsonObject("source"))
|
||||||
movedRef = TootAccountRef.mayNull(
|
movedRef = tootAccountRefOrNull(
|
||||||
parser,
|
parser,
|
||||||
src.jsonObject("moved")?.let {
|
src.jsonObject("moved")?.let {
|
||||||
tootAccount(parser, it)
|
tootAccount(parser, it)
|
||||||
|
@ -692,8 +685,8 @@ open class TootAccount(
|
||||||
acct = acct,
|
acct = acct,
|
||||||
apDomain = apDomain,
|
apDomain = apDomain,
|
||||||
apiHost = apiHost,
|
apiHost = apiHost,
|
||||||
avatar = avatar,
|
avatar = avatar?.replace(reMisskeyIoProxy, "https://"),
|
||||||
avatar_static = avatar_static,
|
avatar_static = avatar_static?.replace(reMisskeyIoProxy, "https://"),
|
||||||
birthday = birthday,
|
birthday = birthday,
|
||||||
bot = bot,
|
bot = bot,
|
||||||
created_at = created_at,
|
created_at = created_at,
|
||||||
|
|
|
@ -18,10 +18,7 @@ class TootAccountRef private constructor(
|
||||||
fun get() = TootAccountMap.find(this)
|
fun get() = TootAccountMap.find(this)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun notNull(parser: TootParser, account: TootAccount) =
|
fun tootAccountRefOrNull(parser: TootParser, account: TootAccount?): TootAccountRef? {
|
||||||
tootAccountRef(parser, account)
|
|
||||||
|
|
||||||
fun mayNull(parser: TootParser, account: TootAccount?): TootAccountRef? {
|
|
||||||
return when (account) {
|
return when (account) {
|
||||||
null -> null
|
null -> null
|
||||||
else -> tootAccountRef(parser, account)
|
else -> tootAccountRef(parser, account)
|
||||||
|
|
|
@ -38,9 +38,7 @@ class TootAnnouncement(
|
||||||
private val log = LogCategory("TootAnnouncement")
|
private val log = LogCategory("TootAnnouncement")
|
||||||
|
|
||||||
fun tootAnnouncement(parser: TootParser, src: JsonObject): TootAnnouncement {
|
fun tootAnnouncement(parser: TootParser, src: JsonObject): TootAnnouncement {
|
||||||
val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
|
val custom_emojis = parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMastodon)
|
||||||
CustomEmoji.decode(parser.apDomain, parser.apiHost, it)
|
|
||||||
}
|
|
||||||
val reactions = parseListOrNull(src.jsonArray("reactions")) {
|
val reactions = parseListOrNull(src.jsonArray("reactions")) {
|
||||||
TootReaction.parseFedibird(it)
|
TootReaction.parseFedibird(it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.api.entity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
|
||||||
import jp.juggler.subwaytooter.pref.PrefB
|
import jp.juggler.subwaytooter.pref.PrefB
|
||||||
import jp.juggler.util.data.JsonObject
|
import jp.juggler.util.data.JsonObject
|
||||||
import jp.juggler.util.data.notEmpty
|
import jp.juggler.util.data.notEmpty
|
||||||
|
@ -81,68 +82,50 @@ class TootNotification(
|
||||||
const val TYPE_STATUS_REFERENCE = "status_reference"
|
const val TYPE_STATUS_REFERENCE = "status_reference"
|
||||||
const val TYPE_SCHEDULED_STATUS = "scheduled_status"
|
const val TYPE_SCHEDULED_STATUS = "scheduled_status"
|
||||||
|
|
||||||
fun tootNotification(parser: TootParser, src: JsonObject): TootNotification {
|
private fun tootNotificationMisskey(parser: TootParser, src: JsonObject): TootNotification {
|
||||||
val id: EntityId
|
// Misskeyの通知APIはページネーションをIDでしか行えない
|
||||||
// One of: "mention", "reblog", "favourite", "follow"
|
// これは改善される予定 https://github.com/syuilo/misskey/issues/2275
|
||||||
val type: String
|
|
||||||
// The Account sending the notification to the user
|
|
||||||
val accountRef: TootAccountRef?
|
|
||||||
|
|
||||||
// The Status associated with the notification, if applicable
|
val created_at: String? = src.string("createdAt")
|
||||||
// 投稿の更新により変更可能になる
|
|
||||||
val status: TootStatus?
|
|
||||||
|
|
||||||
val reaction: TootReaction?
|
val accountRef = tootAccountRefOrNull(
|
||||||
|
|
||||||
val reblog_visibility: TootVisibility
|
|
||||||
|
|
||||||
// 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,
|
||||||
parser.account(
|
parser.account(src.jsonObject("user"))
|
||||||
src.jsonObject("user")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
status = parser.status(
|
|
||||||
src.jsonObject("note")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
reaction = src.string("reaction")
|
val reaction: TootReaction? = src.string("reaction")
|
||||||
?.notEmpty()
|
?.notEmpty()
|
||||||
?.let { TootReaction.parseMisskey(it) }
|
?.let { TootReaction.parseMisskey(it) }
|
||||||
|
|
||||||
reblog_visibility = TootVisibility.Unknown
|
return TootNotification(
|
||||||
|
json = src,
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Misskeyの通知APIはページネーションをIDでしか行えない
|
private fun tootNotificationMastodon(
|
||||||
// これは改善される予定 https://github.com/syuilo/misskey/issues/2275
|
parser: TootParser,
|
||||||
} else {
|
src: JsonObject,
|
||||||
id = EntityId.mayDefault(src.string("id"))
|
): TootNotification {
|
||||||
|
|
||||||
type = src.stringOrThrow("type")
|
val created_at: String? = src.string("created_at")
|
||||||
|
|
||||||
created_at = src.string("created_at")
|
val accountRef: TootAccountRef? =
|
||||||
time_created_at = TootStatus.parseTime(created_at)
|
tootAccountRefOrNull(parser, parser.account(src.jsonObject("account")))
|
||||||
accountRef =
|
|
||||||
TootAccountRef.mayNull(parser, parser.account(src.jsonObject("account")))
|
|
||||||
status = parser.status(src.jsonObject("status"))
|
|
||||||
|
|
||||||
reaction = src.jsonObject("emoji_reaction")
|
val status: TootStatus? = parser.status(src.jsonObject("status"))
|
||||||
|
|
||||||
|
val reaction: TootReaction? = src.jsonObject("emoji_reaction")
|
||||||
?.notEmpty()
|
?.notEmpty()
|
||||||
?.let { TootReaction.parseFedibird(it) }
|
?.let { TootReaction.parseFedibird(it) }
|
||||||
// pleroma unicode emoji
|
?: src.string("emoji")?.let { TootReaction(name = it) } // pleroma unicode emoji
|
||||||
?: src.string("emoji")?.let { TootReaction(name = it) }
|
|
||||||
|
|
||||||
// fedibird
|
// fedibird
|
||||||
// https://github.com/fedibird/mastodon/blob/7974fd3c7ec11ea9f7bef4ad7f4009fff53f62af/app/serializers/rest/notification_serializer.rb#L9
|
// https://github.com/fedibird/mastodon/blob/7974fd3c7ec11ea9f7bef4ad7f4009fff53f62af/app/serializers/rest/notification_serializer.rb#L9
|
||||||
|
@ -150,21 +133,28 @@ class TootNotification(
|
||||||
src.boolean("limited") == true -> "limited"
|
src.boolean("limited") == true -> "limited"
|
||||||
else -> src.string("reblog_visibility")
|
else -> src.string("reblog_visibility")
|
||||||
}
|
}
|
||||||
reblog_visibility = TootVisibility.parseMastodon(visibilityString)
|
|
||||||
|
val reblog_visibility = TootVisibility.parseMastodon(visibilityString)
|
||||||
?: TootVisibility.Unknown
|
?: TootVisibility.Unknown
|
||||||
}
|
|
||||||
return TootNotification(
|
return TootNotification(
|
||||||
json = src,
|
json = src,
|
||||||
id = id,
|
id = EntityId.mayDefault(src.string("id")),
|
||||||
type = type,
|
type = src.stringOrThrow("type"),
|
||||||
accountRef = accountRef,
|
accountRef = accountRef,
|
||||||
status = status,
|
status = status,
|
||||||
reaction = reaction,
|
reaction = reaction,
|
||||||
reblog_visibility = reblog_visibility,
|
reblog_visibility = reblog_visibility,
|
||||||
created_at = created_at,
|
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
|
override fun getOrderId() = id
|
||||||
|
@ -172,16 +162,8 @@ class TootNotification(
|
||||||
fun getNotificationLine(context: Context): String {
|
fun getNotificationLine(context: Context): String {
|
||||||
|
|
||||||
val name = when (PrefB.bpShowAcctInSystemNotification.value) {
|
val name = when (PrefB.bpShowAcctInSystemNotification.value) {
|
||||||
false -> accountRef?.decoded_display_name
|
true -> accountRef?.get()?.acct?.pretty?.notEmpty()?.let { "@$it" }
|
||||||
|
else -> accountRef?.decoded_display_name
|
||||||
true -> {
|
|
||||||
val acctPretty = accountRef?.get()?.acct?.pretty
|
|
||||||
if (acctPretty?.isNotEmpty() == true) {
|
|
||||||
"@$acctPretty"
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: "?"
|
} ?: "?"
|
||||||
|
|
||||||
return when (type) {
|
return when (type) {
|
||||||
|
@ -223,7 +205,10 @@ class TootNotification(
|
||||||
TYPE_EMOJI_REACTION_PLEROMA,
|
TYPE_EMOJI_REACTION_PLEROMA,
|
||||||
TYPE_EMOJI_REACTION,
|
TYPE_EMOJI_REACTION,
|
||||||
TYPE_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_VOTE,
|
||||||
TYPE_POLL_VOTE_MISSKEY,
|
TYPE_POLL_VOTE_MISSKEY,
|
||||||
|
@ -239,7 +224,7 @@ class TootNotification(
|
||||||
TYPE_POLL,
|
TYPE_POLL,
|
||||||
-> context.getString(R.string.end_of_polling_from, name)
|
-> 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.R
|
||||||
import jp.juggler.subwaytooter.api.TootAccountMap
|
import jp.juggler.subwaytooter.api.TootAccountMap
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
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.api.entity.TootAttachment.Companion.tootAttachment
|
||||||
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
import jp.juggler.subwaytooter.emoji.CustomEmoji
|
||||||
import jp.juggler.subwaytooter.mfm.SpannableStringBuilderEx
|
import jp.juggler.subwaytooter.mfm.SpannableStringBuilderEx
|
||||||
|
@ -113,9 +114,9 @@ class TootStatus(
|
||||||
//Application from which the status was posted
|
//Application from which the status was posted
|
||||||
val application: TootApplication?,
|
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
|
// The time the status was created
|
||||||
private val created_at: String?,
|
private val created_at: String?,
|
||||||
|
@ -569,12 +570,19 @@ class TootStatus(
|
||||||
}
|
}
|
||||||
val who = parser.account(src.jsonObject("user"))
|
val who = parser.account(src.jsonObject("user"))
|
||||||
?: error("missing account")
|
?: error("missing account")
|
||||||
val accountRef = TootAccountRef.tootAccountRef(parser, who)
|
val accountRef = tootAccountRef(parser, who)
|
||||||
val account = accountRef.get()
|
val account = accountRef.get()
|
||||||
val created_at = src.string("createdAt")
|
val created_at = src.string("createdAt")
|
||||||
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
|
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
|
||||||
val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
|
var custom_emojis: MutableMap<String, CustomEmoji>? =
|
||||||
CustomEmoji.decodeMisskey(parser.apDomain, parser.apiHost, it)
|
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ということにする
|
// Misskeyは画像毎にNSFWフラグがある。どれか1枚でもNSFWならトゥート全体がNSFWということにする
|
||||||
|
@ -941,9 +949,7 @@ class TootStatus(
|
||||||
val created_at = src.string("created_at")
|
val created_at = src.string("created_at")
|
||||||
|
|
||||||
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
|
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
|
||||||
val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) {
|
val custom_emojis = parseMapOrNull(src.jsonArray("emojis"),CustomEmoji::decodeMastodon)
|
||||||
CustomEmoji.decode(parser.apDomain, parser.apiHost, it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val profile_emojis = when (val o = src["profile_emojis"]) {
|
val profile_emojis = when (val o = src["profile_emojis"]) {
|
||||||
is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) }
|
is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) }
|
||||||
|
|
|
@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.api.finder
|
||||||
|
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
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.table.SavedAccount
|
||||||
import jp.juggler.util.data.JsonArray
|
import jp.juggler.util.data.JsonArray
|
||||||
import jp.juggler.util.data.JsonObject
|
import jp.juggler.util.data.JsonObject
|
||||||
|
@ -23,7 +24,7 @@ private fun misskeyUnwrapRelationAccount(parser: TootParser, srcList: JsonArray,
|
||||||
srcList.objectList().mapNotNull {
|
srcList.objectList().mapNotNull {
|
||||||
when (val relationId = EntityId.mayNull(it.string("id"))) {
|
when (val relationId = EntityId.mayNull(it.string("id"))) {
|
||||||
null -> null
|
null -> null
|
||||||
else -> TootAccountRef.mayNull(parser, parser.account(it.jsonObject(key)))
|
else -> tootAccountRefOrNull(parser, parser.account(it.jsonObject(key)))
|
||||||
?.apply { _orderId = relationId }
|
?.apply { _orderId = relationId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -327,7 +327,7 @@ class Column(
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
accessInfo: SavedAccount,
|
accessInfo: SavedAccount,
|
||||||
type: Int,
|
type: Int,
|
||||||
vararg params: Any,
|
params: Array<out Any>,
|
||||||
) : this(
|
) : this(
|
||||||
appState = appState,
|
appState = appState,
|
||||||
context = appState.context,
|
context = appState.context,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.api.TootApiClient
|
||||||
import jp.juggler.subwaytooter.api.TootApiResult
|
import jp.juggler.subwaytooter.api.TootApiResult
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
import jp.juggler.subwaytooter.api.TootParser
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
import jp.juggler.subwaytooter.api.entity.*
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
|
||||||
import jp.juggler.subwaytooter.columnviewholder.saveScrollPosition
|
import jp.juggler.subwaytooter.columnviewholder.saveScrollPosition
|
||||||
import jp.juggler.util.data.*
|
import jp.juggler.util.data.*
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
|
@ -122,7 +123,7 @@ suspend fun Column.loadProfileAccount(
|
||||||
// ユーザリレーションの取り扱いのため、別のparserを作ってはいけない
|
// ユーザリレーションの取り扱いのため、別のparserを作ってはいけない
|
||||||
parser.misskeyDecodeProfilePin = true
|
parser.misskeyDecodeProfilePin = true
|
||||||
try {
|
try {
|
||||||
TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
tootAccountRefOrNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
||||||
this.whoAccount = a
|
this.whoAccount = a
|
||||||
client.publishApiProgress("") // カラムヘッダの再表示
|
client.publishApiProgress("") // カラムヘッダの再表示
|
||||||
}
|
}
|
||||||
|
@ -134,7 +135,7 @@ suspend fun Column.loadProfileAccount(
|
||||||
else -> client.request(
|
else -> client.request(
|
||||||
"/api/v1/accounts/$profileId"
|
"/api/v1/accounts/$profileId"
|
||||||
)?.also { result1 ->
|
)?.also { result1 ->
|
||||||
TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
tootAccountRefOrNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
||||||
this.whoAccount = a
|
this.whoAccount = a
|
||||||
|
|
||||||
this.whoFeaturedTags = null
|
this.whoFeaturedTags = null
|
||||||
|
|
|
@ -95,21 +95,22 @@ object DlgConfirm {
|
||||||
suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) =
|
suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) =
|
||||||
confirm(getString(messageId, *args))
|
confirm(getString(messageId, *args))
|
||||||
|
|
||||||
suspend fun AppCompatActivity.confirm(message: CharSequence) {
|
suspend fun AppCompatActivity.confirm(message: CharSequence, title: CharSequence? = null) {
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
try {
|
try {
|
||||||
val views = DlgConfirmBinding.inflate(layoutInflater)
|
val views = DlgConfirmBinding.inflate(layoutInflater)
|
||||||
views.tvMessage.text = message
|
views.tvMessage.text = message
|
||||||
views.cbSkipNext.visibility = View.GONE
|
views.cbSkipNext.visibility = View.GONE
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(this)
|
val dialog = AlertDialog.Builder(this).apply {
|
||||||
.setView(views.root)
|
setView(views.root)
|
||||||
.setCancelable(true)
|
setCancelable(true)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
title?.let { setTitle(it) }
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
setNegativeButton(R.string.cancel, null)
|
||||||
|
setPositiveButton(R.string.ok) { _, _ ->
|
||||||
if (cont.isActive) cont.resume(Unit)
|
if (cont.isActive) cont.resume(Unit)
|
||||||
}
|
}
|
||||||
.create()
|
}.create()
|
||||||
dialog.setOnDismissListener {
|
dialog.setOnDismissListener {
|
||||||
if (cont.isActive) cont.resumeWithException(CancellationException("dialog closed."))
|
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.JsonArray
|
||||||
import jp.juggler.util.data.JsonObject
|
import jp.juggler.util.data.JsonObject
|
||||||
import jp.juggler.util.data.notEmpty
|
import jp.juggler.util.data.notEmpty
|
||||||
|
import jp.juggler.util.data.toMutableMap
|
||||||
|
|
||||||
sealed interface EmojiBase
|
sealed interface EmojiBase
|
||||||
|
|
||||||
|
@ -52,7 +53,6 @@ class UnicodeEmoji(
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomEmoji(
|
class CustomEmoji(
|
||||||
val apDomain: Host,
|
|
||||||
val shortcode: String, // shortcode (コロンを含まない)
|
val shortcode: String, // shortcode (コロンを含まない)
|
||||||
val url: String, // 画像URL
|
val url: String, // 画像URL
|
||||||
val staticUrl: String?, // アニメーションなしの画像URL
|
val staticUrl: String?, // アニメーションなしの画像URL
|
||||||
|
@ -63,7 +63,6 @@ class CustomEmoji(
|
||||||
) : EmojiBase, Mappable<String> {
|
) : EmojiBase, Mappable<String> {
|
||||||
|
|
||||||
fun makeAlias(alias: String) = CustomEmoji(
|
fun makeAlias(alias: String) = CustomEmoji(
|
||||||
apDomain = apDomain,
|
|
||||||
shortcode = shortcode,
|
shortcode = shortcode,
|
||||||
url = url,
|
url = url,
|
||||||
staticUrl = staticUrl,
|
staticUrl = staticUrl,
|
||||||
|
@ -80,9 +79,8 @@ class CustomEmoji(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val decode: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src ->
|
fun decodeMastodon(src: JsonObject): CustomEmoji {
|
||||||
CustomEmoji(
|
return CustomEmoji(
|
||||||
apDomain = apDomain,
|
|
||||||
shortcode = src.stringOrThrow("shortcode"),
|
shortcode = src.stringOrThrow("shortcode"),
|
||||||
url = src.stringOrThrow("url"),
|
url = src.stringOrThrow("url"),
|
||||||
staticUrl = src.string("static_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")
|
val url = src.string("url") ?: error("missing url")
|
||||||
|
return CustomEmoji(
|
||||||
CustomEmoji(
|
|
||||||
apDomain = apDomain,
|
|
||||||
shortcode = src.string("name") ?: error("missing name"),
|
shortcode = src.string("name") ?: error("missing name"),
|
||||||
url = url,
|
url = url,
|
||||||
staticUrl = url,
|
staticUrl = url,
|
||||||
aliases = parseAliases(src.jsonArray("aliases")),
|
|
||||||
category = src.string("category"),
|
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 name = src.string("name") ?: error("missing name")
|
||||||
val url = "https://${apiHost.ascii}/emoji/$name.webp"
|
val url = "https://${apiHost.ascii}/emoji/$name.webp"
|
||||||
CustomEmoji(
|
return CustomEmoji(
|
||||||
apDomain = apDomain,
|
|
||||||
shortcode = name,
|
shortcode = name,
|
||||||
url = url,
|
url = url,
|
||||||
staticUrl = 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>? {
|
private fun parseAliases(src: JsonArray?): ArrayList<String>? {
|
||||||
var dst = null as ArrayList<String>?
|
var dst = null as ArrayList<String>?
|
||||||
if (src != null) {
|
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 {
|
fun isEnabled(context: Context): Boolean {
|
||||||
if (Build.VERSION.SDK_INT >= 33) {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
|
@ -175,7 +175,11 @@ enum class NotificationChannels(
|
||||||
text: String? = context.getString(descId),
|
text: String? = context.getString(descId),
|
||||||
piTap: PendingIntent? = null,
|
piTap: PendingIntent? = null,
|
||||||
piDelete: PendingIntent? = null,
|
piDelete: PendingIntent? = null,
|
||||||
|
force:Boolean = false,
|
||||||
): ForegroundInfo? {
|
): ForegroundInfo? {
|
||||||
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
|
if(!force){
|
||||||
if (Build.VERSION.SDK_INT >= 33) {
|
if (Build.VERSION.SDK_INT >= 33) {
|
||||||
if (ActivityCompat.checkSelfPermission(
|
if (ActivityCompat.checkSelfPermission(
|
||||||
context,
|
context,
|
||||||
|
@ -186,11 +190,11 @@ enum class NotificationChannels(
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
|
||||||
if (!notificationManager.isChannelEnabled(id)) {
|
if (!notificationManager.isChannelEnabled(id)) {
|
||||||
log.w("[$id] notification channel is disabled.")
|
log.w("[$id] notification channel is disabled.")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val nc = this
|
val nc = this
|
||||||
val builder = NotificationCompat.Builder(context, nc.id).apply {
|
val builder = NotificationCompat.Builder(context, nc.id).apply {
|
||||||
priority = nc.priority
|
priority = nc.priority
|
||||||
|
|
|
@ -328,6 +328,13 @@ class PollingChecker(
|
||||||
|
|
||||||
val notification = parser.notification(src) ?: return
|
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
|
if (notification.status?.checkMuted() == true) return
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,7 @@ fun AppCompatActivity.resetNotificationTracking(account: SavedAccount) {
|
||||||
}
|
}
|
||||||
launchAndShowError {
|
launchAndShowError {
|
||||||
withContext(AppDispatchers.IO){
|
withContext(AppDispatchers.IO){
|
||||||
daoNotificationShown.cleayByAcct(account.acct.ascii)
|
daoNotificationShown.cleayByAcct(account.acct)
|
||||||
PollingChecker.accountMutex(account.db_id).withLock {
|
PollingChecker.accountMutex(account.db_id).withLock {
|
||||||
daoNotificationTracking.resetTrackingState(account.db_id)
|
daoNotificationTracking.resetTrackingState(account.db_id)
|
||||||
}
|
}
|
||||||
|
@ -232,7 +232,7 @@ suspend fun checkNoticifationAll(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
daoSavedAccount.loadAccountList().mapNotNull { sa ->
|
daoSavedAccount.loadRealAccounts().mapNotNull { sa ->
|
||||||
when {
|
when {
|
||||||
sa.isPseudo || !sa.isConfirmed -> null
|
sa.isPseudo || !sa.isConfirmed -> null
|
||||||
else -> EmptyScope.launch(AppDispatchers.DEFAULT) {
|
else -> EmptyScope.launch(AppDispatchers.DEFAULT) {
|
||||||
|
|
|
@ -158,9 +158,10 @@ class PollingWorker2(
|
||||||
|
|
||||||
private fun messageToForegroundInfo(
|
private fun messageToForegroundInfo(
|
||||||
text: String,
|
text: String,
|
||||||
|
force:Boolean =false
|
||||||
): ForegroundInfo? {
|
): ForegroundInfo? {
|
||||||
// テキストが変化していないなら更新しない
|
// テキストが変化していないなら更新しない
|
||||||
if (text.isEmpty() || text == lastMessage) return null
|
if (!force && (text.isEmpty() || text == lastMessage)) return null
|
||||||
|
|
||||||
lastMessage = text
|
lastMessage = text
|
||||||
log.i(text)
|
log.i(text)
|
||||||
|
@ -181,6 +182,15 @@ class PollingWorker2(
|
||||||
context,
|
context,
|
||||||
text = text,
|
text = text,
|
||||||
piTap = piTap,
|
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(
|
val bpMoveNotificationsQuickFilter = BooleanPref(
|
||||||
"MoveNotificationsQuickFilter",
|
"MoveNotificationsQuickFilter",
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
val bpShowAcctInSystemNotification = BooleanPref(
|
val bpShowAcctInSystemNotification = BooleanPref(
|
||||||
"ShowAcctInSystemNotification",
|
"ShowAcctInSystemNotification",
|
||||||
|
|
|
@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.push
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootNotification
|
||||||
import jp.juggler.subwaytooter.table.PushMessage
|
import jp.juggler.subwaytooter.table.PushMessage
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
|
|
||||||
|
@ -14,42 +15,42 @@ enum class PushMessageIconColor(
|
||||||
val keys: Array<String>,
|
val keys: Array<String>,
|
||||||
) {
|
) {
|
||||||
Favourite(
|
Favourite(
|
||||||
R.color.colorNotificationAccentFavourite,
|
0,
|
||||||
R.drawable.ic_star_outline,
|
R.drawable.ic_star_outline,
|
||||||
arrayOf("favourite"),
|
arrayOf("favourite"),
|
||||||
),
|
),
|
||||||
Mention(
|
Mention(
|
||||||
R.color.colorNotificationAccentMention,
|
0,
|
||||||
R.drawable.outline_alternate_email_24,
|
R.drawable.outline_alternate_email_24,
|
||||||
arrayOf("mention"),
|
arrayOf("mention"),
|
||||||
),
|
),
|
||||||
Reply(
|
Reply(
|
||||||
R.color.colorNotificationAccentReply,
|
0,
|
||||||
R.drawable.ic_reply,
|
R.drawable.ic_reply,
|
||||||
arrayOf("reply")
|
arrayOf("reply")
|
||||||
),
|
),
|
||||||
Reblog(
|
Reblog(
|
||||||
R.color.colorNotificationAccentReblog,
|
0,
|
||||||
R.drawable.ic_repeat,
|
R.drawable.ic_repeat,
|
||||||
arrayOf("reblog", "renote"),
|
arrayOf("reblog", "renote"),
|
||||||
),
|
),
|
||||||
Quote(
|
Quote(
|
||||||
R.color.colorNotificationAccentQuote,
|
0,
|
||||||
R.drawable.ic_quote,
|
R.drawable.ic_quote,
|
||||||
arrayOf("quote"),
|
arrayOf("quote"),
|
||||||
),
|
),
|
||||||
Follow(
|
Follow(
|
||||||
R.color.colorNotificationAccentFollow,
|
0,
|
||||||
R.drawable.ic_person_add,
|
R.drawable.ic_person_add,
|
||||||
arrayOf("follow", "followRequestAccepted")
|
arrayOf("follow", "followRequestAccepted")
|
||||||
),
|
),
|
||||||
Unfollow(
|
Unfollow(
|
||||||
R.color.colorNotificationAccentUnfollow,
|
0,
|
||||||
R.drawable.ic_follow_cross,
|
R.drawable.ic_follow_cross,
|
||||||
arrayOf("unfollow")
|
arrayOf("unfollow")
|
||||||
),
|
),
|
||||||
Reaction(
|
Reaction(
|
||||||
R.color.colorNotificationAccentReaction,
|
0,
|
||||||
R.drawable.outline_add_reaction_24,
|
R.drawable.outline_add_reaction_24,
|
||||||
arrayOf("reaction", "emoji_reaction", "pleroma:emoji_reaction")
|
arrayOf("reaction", "emoji_reaction", "pleroma:emoji_reaction")
|
||||||
),
|
),
|
||||||
|
@ -59,25 +60,30 @@ enum class PushMessageIconColor(
|
||||||
arrayOf("follow_request", "receiveFollowRequest"),
|
arrayOf("follow_request", "receiveFollowRequest"),
|
||||||
),
|
),
|
||||||
Poll(
|
Poll(
|
||||||
R.color.colorNotificationAccentPoll,
|
0,
|
||||||
R.drawable.outline_poll_24,
|
R.drawable.outline_poll_24,
|
||||||
arrayOf("pollVote", "poll_vote", "poll"),
|
arrayOf("pollVote", "poll_vote", "poll"),
|
||||||
),
|
),
|
||||||
Status(
|
Status(
|
||||||
R.color.colorNotificationAccentStatus,
|
0,
|
||||||
R.drawable.ic_edit,
|
R.drawable.ic_edit,
|
||||||
arrayOf("status", "update", "status_reference")
|
arrayOf("status", "update", "status_reference")
|
||||||
),
|
),
|
||||||
SignUp(
|
AdminSignUp(
|
||||||
R.color.colorNotificationAccentSignUp,
|
0,
|
||||||
R.drawable.outline_group_add_24,
|
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(
|
Unknown(
|
||||||
R.color.colorNotificationAccentUnknown,
|
R.color.colorNotificationAccentUnknown,
|
||||||
R.drawable.ic_question,
|
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.R
|
||||||
import jp.juggler.subwaytooter.api.ApiError
|
import jp.juggler.subwaytooter.api.ApiError
|
||||||
import jp.juggler.subwaytooter.api.TootParser
|
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.TootNotification
|
||||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
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.api.push.ApiPushMisskey
|
||||||
import jp.juggler.subwaytooter.pref.PrefDevice
|
import jp.juggler.subwaytooter.pref.PrefDevice
|
||||||
import jp.juggler.subwaytooter.pref.lazyContext
|
import jp.juggler.subwaytooter.pref.lazyContext
|
||||||
|
@ -172,11 +174,28 @@ class PushMisskey(
|
||||||
|
|
||||||
when (val eventType = json.string("type")) {
|
when (val eventType = json.string("type")) {
|
||||||
"notification" -> {
|
"notification" -> {
|
||||||
|
val body = json.jsonObject("body")
|
||||||
|
?: error("missing body of notification")
|
||||||
val parser = TootParser(context, a)
|
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) {
|
if (notification.status?.checkMuted() == true) {
|
||||||
|
@ -191,7 +210,7 @@ class PushMisskey(
|
||||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||||
-> {
|
-> {
|
||||||
val whoAcct = a.getFullAcct(user)
|
val whoAcct = a.getFullAcct(who)
|
||||||
if (TootStatus.favMuteSet?.contains(whoAcct) == true) {
|
if (TootStatus.favMuteSet?.contains(whoAcct) == true) {
|
||||||
error("muted by favMuteSet ${whoAcct.pretty}")
|
error("muted by favMuteSet ${whoAcct.pretty}")
|
||||||
}
|
}
|
||||||
|
@ -200,8 +219,9 @@ class PushMisskey(
|
||||||
|
|
||||||
// バッジ画像のURLはない。通知種別により決まる
|
// バッジ画像のURLはない。通知種別により決まる
|
||||||
pm.iconSmall = null
|
pm.iconSmall = null
|
||||||
pm.iconLarge = a.supplyBaseUrl(user?.avatar_static)
|
pm.iconLarge = a.supplyBaseUrl(who?.avatar_static)
|
||||||
pm.notificationType = notification.type
|
pm.notificationType = notification.type
|
||||||
|
pm.notificationId = notification.id.toString()
|
||||||
|
|
||||||
json.long("dateTime")?.let { pm.timestamp = it }
|
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.crypt.*
|
||||||
import jp.juggler.subwaytooter.ActCallback
|
import jp.juggler.subwaytooter.ActCallback
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.api.entity.Acct
|
|
||||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||||
import jp.juggler.subwaytooter.api.push.ApiPushAppServer
|
import jp.juggler.subwaytooter.api.push.ApiPushAppServer
|
||||||
import jp.juggler.subwaytooter.api.push.ApiPushMastodon
|
import jp.juggler.subwaytooter.api.push.ApiPushMastodon
|
||||||
|
@ -46,19 +45,23 @@ import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private val log = LogCategory("PushRepo")
|
private val log = LogCategory("PushRepo")
|
||||||
|
|
||||||
val Context.pushRepo: PushRepo
|
private val defaultOkHttp by lazy {
|
||||||
get() {
|
OkHttpClient.Builder().apply {
|
||||||
val okHttp = OkHttpClient.Builder().apply {
|
|
||||||
connectTimeout(60, TimeUnit.SECONDS)
|
connectTimeout(60, TimeUnit.SECONDS)
|
||||||
writeTimeout(60, TimeUnit.SECONDS)
|
writeTimeout(60, TimeUnit.SECONDS)
|
||||||
readTimeout(60, TimeUnit.SECONDS)
|
readTimeout(60, TimeUnit.SECONDS)
|
||||||
}.build()
|
}.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val Context.pushRepo: PushRepo
|
||||||
|
get() {
|
||||||
|
val okHttp = defaultOkHttp
|
||||||
val appDatabase = appDatabase
|
val appDatabase = appDatabase
|
||||||
return PushRepo(
|
return PushRepo(
|
||||||
context = applicationContextSafe,
|
context = applicationContextSafe,
|
||||||
apiPushAppServer = ApiPushAppServer(okHttp),
|
apiAppServer = ApiPushAppServer(okHttp),
|
||||||
apiPushMastodon = ApiPushMastodon(okHttp),
|
apiMastodon = ApiPushMastodon(okHttp),
|
||||||
apiPushMisskey = ApiPushMisskey(okHttp),
|
apiMisskey = ApiPushMisskey(okHttp),
|
||||||
daoSavedAccount = SavedAccount.Access(appDatabase, this),
|
daoSavedAccount = SavedAccount.Access(appDatabase, this),
|
||||||
daoPushMessage = PushMessage.Access(appDatabase),
|
daoPushMessage = PushMessage.Access(appDatabase),
|
||||||
daoStatus = AccountNotificationStatus.Access(appDatabase),
|
daoStatus = AccountNotificationStatus.Access(appDatabase),
|
||||||
|
@ -70,9 +73,9 @@ val Context.pushRepo: PushRepo
|
||||||
|
|
||||||
class PushRepo(
|
class PushRepo(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val apiPushMastodon: ApiPushMastodon,
|
private val apiMastodon: ApiPushMastodon,
|
||||||
private val apiPushMisskey: ApiPushMisskey,
|
private val apiMisskey: ApiPushMisskey,
|
||||||
private val apiPushAppServer: ApiPushAppServer,
|
private val apiAppServer: ApiPushAppServer,
|
||||||
private val daoSavedAccount: SavedAccount.Access,
|
private val daoSavedAccount: SavedAccount.Access,
|
||||||
private val daoPushMessage: PushMessage.Access,
|
private val daoPushMessage: PushMessage.Access,
|
||||||
private val daoStatus: AccountNotificationStatus.Access,
|
private val daoStatus: AccountNotificationStatus.Access,
|
||||||
|
@ -92,7 +95,7 @@ class PushRepo(
|
||||||
private val pushMisskey by lazy {
|
private val pushMisskey by lazy {
|
||||||
PushMisskey(
|
PushMisskey(
|
||||||
context = context,
|
context = context,
|
||||||
api = apiPushMisskey,
|
api = apiMisskey,
|
||||||
provider = provider,
|
provider = provider,
|
||||||
prefDevice = prefDevice,
|
prefDevice = prefDevice,
|
||||||
daoStatus = daoStatus,
|
daoStatus = daoStatus,
|
||||||
|
@ -102,7 +105,7 @@ class PushRepo(
|
||||||
private val pushMastodon by lazy {
|
private val pushMastodon by lazy {
|
||||||
PushMastodon(
|
PushMastodon(
|
||||||
context = context,
|
context = context,
|
||||||
api = apiPushMastodon,
|
api = apiMastodon,
|
||||||
provider = provider,
|
provider = provider,
|
||||||
prefDevice = prefDevice,
|
prefDevice = prefDevice,
|
||||||
daoStatus = daoStatus,
|
daoStatus = daoStatus,
|
||||||
|
@ -220,7 +223,7 @@ class PushRepo(
|
||||||
prefDevice.fcmTokenExpired.notEmpty()?.let {
|
prefDevice.fcmTokenExpired.notEmpty()?.let {
|
||||||
refReporter?.get()?.setMessage("期限切れのFCMデバイストークンをアプリサーバから削除しています")
|
refReporter?.get()?.setMessage("期限切れのFCMデバイストークンをアプリサーバから削除しています")
|
||||||
log.i("remove fcmTokenExpired")
|
log.i("remove fcmTokenExpired")
|
||||||
apiPushAppServer.endpointRemove(fcmToken = it)
|
apiAppServer.endpointRemove(fcmToken = it)
|
||||||
prefDevice.fcmTokenExpired = null
|
prefDevice.fcmTokenExpired = null
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
|
@ -232,15 +235,15 @@ class PushRepo(
|
||||||
prefDevice.upEndpointExpired.notEmpty()?.let {
|
prefDevice.upEndpointExpired.notEmpty()?.let {
|
||||||
refReporter?.get()?.setMessage("期限切れのUnifiedPushエンドポイントをアプリサーバから削除しています")
|
refReporter?.get()?.setMessage("期限切れのUnifiedPushエンドポイントをアプリサーバから削除しています")
|
||||||
log.i("remove upEndpointExpired")
|
log.i("remove upEndpointExpired")
|
||||||
apiPushAppServer.endpointRemove(upUrl = it)
|
apiAppServer.endpointRemove(upUrl = it)
|
||||||
prefDevice.upEndpointExpired = null
|
prefDevice.upEndpointExpired = null
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
log.w(ex, "can't forgot upEndpointExpired")
|
log.w(ex, "can't forgot upEndpointExpired")
|
||||||
}
|
}
|
||||||
|
|
||||||
val realAccounts = daoSavedAccount.loadAccountList()
|
val realAccounts = daoSavedAccount.loadRealAccounts()
|
||||||
.filter { !it.isPseudo }
|
.filter { !it.isPseudo && it.isConfirmed }
|
||||||
|
|
||||||
val accts = realAccounts.map { it.acct }
|
val accts = realAccounts.map { it.acct }
|
||||||
|
|
||||||
|
@ -269,23 +272,34 @@ class PushRepo(
|
||||||
prefDevice.pushDistributor = null
|
prefDevice.pushDistributor = null
|
||||||
}
|
}
|
||||||
|
|
||||||
log.i("pushDistributor=${prefDevice.pushDistributor}")
|
|
||||||
val acctHashList = acctHashMap.keys.toList()
|
val acctHashList = acctHashMap.keys.toList()
|
||||||
|
|
||||||
val json = when (prefDevice.pushDistributor) {
|
val json = when (prefDevice.pushDistributor) {
|
||||||
null, "" -> when {
|
null, "" -> when {
|
||||||
fcmHandler.hasFcm -> registerEndpointFcm(acctHashList)
|
fcmHandler.hasFcm -> {
|
||||||
|
log.i("registerEndpoint dist=FCM(default), acctHashList=${acctHashList.size}")
|
||||||
|
registerEndpointFcm(acctHashList)
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
log.w("pushDistributor not selected. but can't select default distributor from background service.")
|
log.w("pushDistributor not selected. but can't select default distributor from background service.")
|
||||||
null
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PrefDevice.PUSH_DISTRIBUTOR_NONE -> {
|
PrefDevice.PUSH_DISTRIBUTOR_NONE -> {
|
||||||
|
log.i("push distrobuter 'none' is selected. it will remove subscription.")
|
||||||
willRemoveSubscription = true
|
willRemoveSubscription = true
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
PrefDevice.PUSH_DISTRIBUTOR_FCM -> registerEndpointFcm(acctHashList)
|
PrefDevice.PUSH_DISTRIBUTOR_FCM -> {
|
||||||
else -> registerEndpointUnifiedPush(acctHashList)
|
log.i("registerEndpoint dist=FCM, acctHashList=${acctHashList.size}")
|
||||||
|
registerEndpointFcm(acctHashList)
|
||||||
}
|
}
|
||||||
|
else -> {
|
||||||
|
log.i("registerEndpoint dist=${prefDevice.pushDistributor}, acctHashList=${acctHashList.size}")
|
||||||
|
registerEndpointUnifiedPush(acctHashList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when {
|
when {
|
||||||
json.isNullOrEmpty() ->
|
json.isNullOrEmpty() ->
|
||||||
log.i("no information of appServerHash.")
|
log.i("no information of appServerHash.")
|
||||||
|
@ -355,8 +369,7 @@ class PushRepo(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
log.i("endpointUpsert up ")
|
apiAppServer.endpointUpsert(
|
||||||
apiPushAppServer.endpointUpsert(
|
|
||||||
upUrl = upEndpoint,
|
upUrl = upEndpoint,
|
||||||
fcmToken = null,
|
fcmToken = null,
|
||||||
acctHashList = acctHashList
|
acctHashList = acctHashList
|
||||||
|
@ -371,8 +384,7 @@ class PushRepo(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
log.i("endpointUpsert fcm ")
|
apiAppServer.endpointUpsert(
|
||||||
apiPushAppServer.endpointUpsert(
|
|
||||||
upUrl = null,
|
upUrl = null,
|
||||||
fcmToken = fcmToken,
|
fcmToken = fcmToken,
|
||||||
acctHashList = acctHashList
|
acctHashList = acctHashList
|
||||||
|
@ -436,7 +448,7 @@ class PushRepo(
|
||||||
*
|
*
|
||||||
* - 実際のアプリでは解読できたものだけを保存したいが、これは試験アプリなので…
|
* - 実際のアプリでは解読できたものだけを保存したいが、これは試験アプリなので…
|
||||||
*/
|
*/
|
||||||
suspend fun reDecode(pm: PushMessage) {
|
suspend fun reprocess(pm: PushMessage) {
|
||||||
withContext(AppDispatchers.IO) {
|
withContext(AppDispatchers.IO) {
|
||||||
updateMessage(pm.id, allowDupilicateNotification = true)
|
updateMessage(pm.id, allowDupilicateNotification = true)
|
||||||
}
|
}
|
||||||
|
@ -463,7 +475,7 @@ class PushRepo(
|
||||||
// アプリサーバから読み直す
|
// アプリサーバから読み直す
|
||||||
if (map["b"] == null) {
|
if (map["b"] == null) {
|
||||||
map.string("l")?.let { largeObjectId ->
|
map.string("l")?.let { largeObjectId ->
|
||||||
apiPushAppServer.getLargeObject(largeObjectId)
|
apiAppServer.getLargeObject(largeObjectId)
|
||||||
?.let {
|
?.let {
|
||||||
map = it.decodeBinPack() as? BinPackMap
|
map = it.decodeBinPack() as? BinPackMap
|
||||||
?: error("binPack decode failed.")
|
?: error("binPack decode failed.")
|
||||||
|
@ -479,14 +491,14 @@ class PushRepo(
|
||||||
val status = daoStatus.findByAcctHash(acctHash)
|
val status = daoStatus.findByAcctHash(acctHash)
|
||||||
?: error("missing status for acctHash $acctHash")
|
?: error("missing status for acctHash $acctHash")
|
||||||
|
|
||||||
val acct = status.acct.notEmpty()
|
val acct = status.acct.takeIf { it.isValidFull }
|
||||||
?: error("empty acct.")
|
?: error("empty acct.")
|
||||||
|
|
||||||
val account = daoSavedAccount.loadAccountByAcct(Acct.parse(acct))
|
|
||||||
?: error("missing account for acct ${status.acct}")
|
|
||||||
|
|
||||||
pm.loginAcct = status.acct
|
pm.loginAcct = status.acct
|
||||||
|
|
||||||
|
val account = daoSavedAccount.loadAccountByAcct(acct)
|
||||||
|
?: error("missing account for acct ${status.acct}")
|
||||||
|
|
||||||
decodeMessageContent(status, pm, map)
|
decodeMessageContent(status, pm, map)
|
||||||
val messageJson = pm.messageJson
|
val messageJson = pm.messageJson
|
||||||
|
|
||||||
|
@ -496,7 +508,7 @@ class PushRepo(
|
||||||
// メッセージに含まれるappServerHashを指定してendpoint登録を削除する
|
// メッセージに含まれるappServerHashを指定してendpoint登録を削除する
|
||||||
// するとアプリサーバはSNSサーバに対してgoneを返すようになり掃除が適切に行われるはず
|
// するとアプリサーバはSNSサーバに対してgoneを返すようになり掃除が適切に行われるはず
|
||||||
map.string("c").notEmpty()?.let {
|
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")
|
log.w("endpointRemove $count hashId=$it")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -504,29 +516,37 @@ class PushRepo(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mastodonはなぜかアクセストークンが書いてあるので危険…
|
// Mastodonはなぜかアクセストークンが書いてあるので危険…
|
||||||
val censored = messageJson.toString()
|
val messageJsonFiltered = messageJson.toString()
|
||||||
.replace(
|
.replace(
|
||||||
""""access_token":"[^"]+"""".toRegex(),
|
""""access_token":"[^"]+"""".toRegex(),
|
||||||
""""access_token":"***""""
|
""""access_token":"***""""
|
||||||
)
|
)
|
||||||
log.i("${status.acct} $censored")
|
log.i("${status.acct} $messageJsonFiltered")
|
||||||
|
|
||||||
|
// ミュート用データを時々読む
|
||||||
|
TootStatus.updateMuteData()
|
||||||
|
|
||||||
// messageJsonを解釈して通知に出す内容を決める
|
// messageJsonを解釈して通知に出す内容を決める
|
||||||
TootStatus.updateMuteData()
|
|
||||||
pushBase(account).formatPushMessage(account, pm)
|
pushBase(account).formatPushMessage(account, pm)
|
||||||
|
|
||||||
val notificationId = pm.notificationId
|
val notificationId = pm.notificationId
|
||||||
if (notificationId.isNullOrEmpty()) {
|
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 &&
|
if (!allowDupilicateNotification &&
|
||||||
daoNotificationShown.duplicateOrPut(acct, notificationId)
|
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)
|
showPushNotification(pm, account, notificationId)
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
log.e(ex, "updateMessage failed.")
|
log.e(ex, "updateMessage failed.")
|
||||||
|
@ -620,8 +640,8 @@ class PushRepo(
|
||||||
account: SavedAccount,
|
account: SavedAccount,
|
||||||
notificationId: String,
|
notificationId: String,
|
||||||
) {
|
) {
|
||||||
if (ncPushMessage.isDissabled(context)) {
|
if (ncPushMessage.isDisabled(context)) {
|
||||||
log.w("ncPushMessage isDissabled.")
|
log.w("ncPushMessage isDisabled.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -676,10 +696,14 @@ class PushRepo(
|
||||||
// val piTap = PendingIntent.getActivity(this, nc.pircTap, iTap, PendingIntent.FLAG_IMMUTABLE)
|
// val piTap = PendingIntent.getActivity(this, nc.pircTap, iTap, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
ncPushMessage.notify(context, urlDelete) {
|
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)
|
setSmallIcon(iconSmall)
|
||||||
iconBitmapLarge?.let { setLargeIcon(it) }
|
iconBitmapLarge?.let { setLargeIcon(it) }
|
||||||
setContentTitle(pm.loginAcct)
|
setContentTitle(pm.loginAcct?.pretty)
|
||||||
setContentText(pm.text)
|
setContentText(pm.text)
|
||||||
setWhen(pm.timestamp)
|
setWhen(pm.timestamp)
|
||||||
setContentIntent(piTap)
|
setContentIntent(piTap)
|
||||||
|
@ -688,6 +712,8 @@ class PushRepo(
|
||||||
pm.textExpand.notEmpty()?.let {
|
pm.textExpand.notEmpty()?.let {
|
||||||
setStyle(NotificationCompat.BigTextStyle().bigText(it))
|
setStyle(NotificationCompat.BigTextStyle().bigText(it))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setGroup(context.packageName + ":" + account.acct.ascii)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
package jp.juggler.subwaytooter.push
|
package jp.juggler.subwaytooter.push
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
|
import jp.juggler.subwaytooter.ActMain
|
||||||
import jp.juggler.subwaytooter.notification.NotificationChannels
|
import jp.juggler.subwaytooter.notification.NotificationChannels
|
||||||
import jp.juggler.util.coroutine.AppDispatchers
|
import jp.juggler.util.coroutine.AppDispatchers
|
||||||
import jp.juggler.util.data.notEmpty
|
import jp.juggler.util.data.notEmpty
|
||||||
|
@ -15,6 +18,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LogCategory("PushWorker")
|
private val log = LogCategory("PushWorker")
|
||||||
|
|
||||||
|
private val ncPushWorker = NotificationChannels.PushWorker
|
||||||
|
|
||||||
const val KEY_ACTION = "action"
|
const val KEY_ACTION = "action"
|
||||||
const val KEY_ENDPOINT = "endpoint"
|
const val KEY_ENDPOINT = "endpoint"
|
||||||
const val KEY_MESSAGE_ID = "messageId"
|
const val KEY_MESSAGE_ID = "messageId"
|
||||||
|
@ -31,8 +36,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
||||||
|
|
||||||
fun enqueueUpEndpoint(context: Context, endpoint: String) {
|
fun enqueueUpEndpoint(context: Context, endpoint: String) {
|
||||||
workDataOf(
|
workDataOf(
|
||||||
PushWorker.KEY_ACTION to PushWorker.ACTION_UP_ENDPOINT,
|
KEY_ACTION to ACTION_UP_ENDPOINT,
|
||||||
PushWorker.KEY_ENDPOINT to endpoint,
|
KEY_ENDPOINT to endpoint,
|
||||||
).launchPushWorker(context)
|
).launchPushWorker(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +50,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
||||||
|
|
||||||
fun enqueuePushMessage(context: Context, messageId: Long) {
|
fun enqueuePushMessage(context: Context, messageId: Long) {
|
||||||
workDataOf(
|
workDataOf(
|
||||||
PushWorker.KEY_ACTION to PushWorker.ACTION_MESSAGE,
|
KEY_ACTION to ACTION_MESSAGE,
|
||||||
PushWorker.KEY_MESSAGE_ID to messageId,
|
KEY_MESSAGE_ID to messageId,
|
||||||
).launchPushWorker(context)
|
).launchPushWorker(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,9 +73,7 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result = try {
|
override suspend fun doWork(): Result = try {
|
||||||
NotificationChannels.PushWorker.createForegroundInfo(
|
createForegroundInfo()?.let { setForegroundAsync(it) }
|
||||||
applicationContext,
|
|
||||||
)?.let{setForegroundAsync(it)}
|
|
||||||
withContext(AppDispatchers.IO) {
|
withContext(AppDispatchers.IO) {
|
||||||
val pushRepo = applicationContext.pushRepo
|
val pushRepo = applicationContext.pushRepo
|
||||||
when (val action = inputData.getString(KEY_ACTION)) {
|
when (val action = inputData.getString(KEY_ACTION)) {
|
||||||
|
@ -106,4 +109,32 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) :
|
||||||
log.e(ex, "doWork failed.")
|
log.e(ex, "doWork failed.")
|
||||||
Result.failure()
|
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.Paint
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.text.style.ReplacementSpan
|
import android.text.style.ReplacementSpan
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.IntRange
|
import androidx.annotation.IntRange
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import jp.juggler.apng.ApngFrames
|
import jp.juggler.apng.ApngFrames
|
||||||
import jp.juggler.subwaytooter.App1
|
import jp.juggler.subwaytooter.App1
|
||||||
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.pref.PrefB
|
import jp.juggler.subwaytooter.pref.PrefB
|
||||||
|
import jp.juggler.subwaytooter.pref.lazyContext
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
class NetworkEmojiSpan internal constructor(
|
class NetworkEmojiSpan internal constructor(
|
||||||
private val url: String,
|
private val url: String,
|
||||||
private val scale: Float = 1f,
|
private val scale: Float = 1f,
|
||||||
|
@DrawableRes private val errorDrawableId: Int = R.drawable.outline_broken_image_24,
|
||||||
) : ReplacementSpan(), AnimatableSpan {
|
) : ReplacementSpan(), AnimatableSpan {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -25,20 +31,19 @@ class NetworkEmojiSpan internal constructor(
|
||||||
private const val descentRatio = 0.211f
|
private const val descentRatio = 0.211f
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mPaint = Paint()
|
private val mPaint = Paint().apply { isFilterBitmap = true }
|
||||||
private val rectSrc = Rect()
|
private val rectSrc = Rect()
|
||||||
private val rectDst = RectF()
|
private val rectDst = RectF()
|
||||||
|
|
||||||
// フレーム探索結果を格納する構造体を確保しておく
|
// フレーム探索結果を格納する構造体を確保しておく
|
||||||
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
private val mFrameFindResult = ApngFrames.FindFrameResult()
|
||||||
|
|
||||||
init {
|
|
||||||
mPaint.isFilterBitmap = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private var invalidateCallback: AnimatableSpanInvalidator? = null
|
private var invalidateCallback: AnimatableSpanInvalidator? = null
|
||||||
|
|
||||||
private var refDrawTarget: WeakReference<Any>? = null
|
private var refDrawTarget: WeakReference<Any>? = null
|
||||||
|
|
||||||
|
private var errorDrawableCache: Drawable? = null
|
||||||
|
|
||||||
override fun setInvalidateCallback(
|
override fun setInvalidateCallback(
|
||||||
drawTargetTag: Any,
|
drawTargetTag: Any,
|
||||||
invalidateCallback: AnimatableSpanInvalidator,
|
invalidateCallback: AnimatableSpanInvalidator,
|
||||||
|
@ -77,16 +82,26 @@ class NetworkEmojiSpan internal constructor(
|
||||||
bottom: Int,
|
bottom: Int,
|
||||||
textPaint: Paint,
|
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
|
val invalidateCallback = this.invalidateCallback
|
||||||
if (invalidateCallback == null) {
|
if (invalidateCallback == null) {
|
||||||
log.e("draw: invalidate_callback is null.")
|
log.e("draw: invalidate_callback is null.")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// APNGデータの取得
|
// APNGデータの取得
|
||||||
val frames = App1.custom_emoji_cache.getFrames(refDrawTarget, url) {
|
val frames = App1.custom_emoji_cache.getFrames(refDrawTarget, url) {
|
||||||
invalidateCallback.delayInvalidate(0L)
|
invalidateCallback.delayInvalidate(0L)
|
||||||
} ?: return
|
} ?: return false
|
||||||
|
|
||||||
val t = when {
|
val t = when {
|
||||||
PrefB.bpDisableEmojiAnimation.value -> 0L
|
PrefB.bpDisableEmojiAnimation.value -> 0L
|
||||||
|
@ -99,13 +114,13 @@ class NetworkEmojiSpan internal constructor(
|
||||||
val b = mFrameFindResult.bitmap
|
val b = mFrameFindResult.bitmap
|
||||||
if (b == null || b.isRecycled) {
|
if (b == null || b.isRecycled) {
|
||||||
log.e("draw: bitmap is null or recycled.")
|
log.e("draw: bitmap is null or recycled.")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
val srcWidth = b.width
|
val srcWidth = b.width
|
||||||
val srcHeight = b.height
|
val srcHeight = b.height
|
||||||
if (srcWidth < 1 || srcHeight < 1) {
|
if (srcWidth < 1 || srcHeight < 1) {
|
||||||
log.e("draw: bitmap size is too small.")
|
log.e("draw: bitmap size is too small.")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
rectSrc.set(0, 0, srcWidth, srcHeight)
|
rectSrc.set(0, 0, srcWidth, srcHeight)
|
||||||
|
|
||||||
|
@ -155,5 +170,67 @@ class NetworkEmojiSpan internal constructor(
|
||||||
if (delay != Long.MAX_VALUE && !PrefB.bpDisableEmojiAnimation.value) {
|
if (delay != Long.MAX_VALUE && !PrefB.bpDisableEmojiAnimation.value) {
|
||||||
invalidateCallback.delayInvalidate(delay)
|
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.TootApiClient
|
||||||
import jp.juggler.subwaytooter.api.TootApiResult
|
import jp.juggler.subwaytooter.api.TootApiResult
|
||||||
import jp.juggler.subwaytooter.api.entity.*
|
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.onStatusRemoved
|
||||||
import jp.juggler.subwaytooter.column.reloadFilter
|
import jp.juggler.subwaytooter.column.reloadFilter
|
||||||
import jp.juggler.subwaytooter.column.replaceStatus
|
import jp.juggler.subwaytooter.column.replaceStatus
|
||||||
|
@ -201,13 +202,7 @@ class StreamConnection(
|
||||||
log.e("$name handleMisskeyMessage: noteUpdated body is null")
|
log.e("$name handleMisskeyMessage: noteUpdated body is null")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fireNoteUpdated(
|
fireNoteUpdated(misskeyNoteUpdate(body), channelId)
|
||||||
MisskeyNoteUpdate(
|
|
||||||
acctGroup.account.apDomain,
|
|
||||||
acctGroup.account.apiHost,
|
|
||||||
body
|
|
||||||
), channelId
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"notification" -> {
|
"notification" -> {
|
||||||
|
|
|
@ -12,7 +12,7 @@ class AccountNotificationStatus(
|
||||||
// DB上のID
|
// DB上のID
|
||||||
var id: Long = 0L,
|
var id: Long = 0L,
|
||||||
// 該当ユーザのacct
|
// 該当ユーザのacct
|
||||||
var acct: String = "",
|
var acct: Acct = Acct.UNKNOWN,
|
||||||
// acctのハッシュ値
|
// acctのハッシュ値
|
||||||
var acctHash: String = "",
|
var acctHash: String = "",
|
||||||
// アプリサーバから受け取ったハッシュ
|
// アプリサーバから受け取ったハッシュ
|
||||||
|
@ -100,7 +100,7 @@ class AccountNotificationStatus(
|
||||||
cursor ?: error("cursor is null!")
|
cursor ?: error("cursor is null!")
|
||||||
AccountNotificationStatus(
|
AccountNotificationStatus(
|
||||||
id = cursor.getLong(idxId),
|
id = cursor.getLong(idxId),
|
||||||
acct = cursor.getString(idxAcct),
|
acct = Acct.parse(cursor.getString(idxAcct)),
|
||||||
acctHash = cursor.getString(idxAcctHash),
|
acctHash = cursor.getString(idxAcctHash),
|
||||||
appServerHash = cursor.getStringOrNull(idxAppServerHash),
|
appServerHash = cursor.getStringOrNull(idxAppServerHash),
|
||||||
pushKeyPrivate = cursor.getBlobOrNull(idxPushKeyPrivate),
|
pushKeyPrivate = cursor.getBlobOrNull(idxPushKeyPrivate),
|
||||||
|
@ -119,7 +119,7 @@ class AccountNotificationStatus(
|
||||||
|
|
||||||
// ID以外のカラムをContentValuesに変換する
|
// ID以外のカラムをContentValuesに変換する
|
||||||
fun toContentValues() = ContentValues().apply {
|
fun toContentValues() = ContentValues().apply {
|
||||||
put(COL_ACCT, acct)
|
put(COL_ACCT, acct.ascii)
|
||||||
put(COL_ACCT_HASH, acctHash)
|
put(COL_ACCT_HASH, acctHash)
|
||||||
put(COL_APP_SERVER_HASH, appServerHash)
|
put(COL_APP_SERVER_HASH, appServerHash)
|
||||||
put(COL_PUSH_KEY_PRIVATE, pushKeyPrivate)
|
put(COL_PUSH_KEY_PRIVATE, pushKeyPrivate)
|
||||||
|
@ -154,7 +154,7 @@ class AccountNotificationStatus(
|
||||||
|
|
||||||
private fun newInstance(acct: Acct) =
|
private fun newInstance(acct: Acct) =
|
||||||
AccountNotificationStatus(
|
AccountNotificationStatus(
|
||||||
acct = acct.ascii,
|
acct = acct,
|
||||||
acctHash = acct.ascii.encodeUTF8().digestSHA256().encodeBase64Url()
|
acctHash = acct.ascii.encodeUTF8().digestSHA256().encodeBase64Url()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.ContentValues
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.provider.BaseColumns
|
import android.provider.BaseColumns
|
||||||
import jp.juggler.subwaytooter.api.entity.Acct
|
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.MetaColumns
|
||||||
import jp.juggler.util.data.TableCompanion
|
import jp.juggler.util.data.TableCompanion
|
||||||
import jp.juggler.util.data.replaceTo
|
import jp.juggler.util.data.replaceTo
|
||||||
|
@ -15,7 +16,6 @@ class NotificationShown(
|
||||||
var acct: String = "",
|
var acct: String = "",
|
||||||
var notificationId: String = "",
|
var notificationId: String = "",
|
||||||
var timeCreate: Long = System.currentTimeMillis(),
|
var timeCreate: Long = System.currentTimeMillis(),
|
||||||
var timeDismiss: Long = 0L,
|
|
||||||
) {
|
) {
|
||||||
companion object : TableCompanion {
|
companion object : TableCompanion {
|
||||||
private val log = LogCategory("NotificationShown")
|
private val log = LogCategory("NotificationShown")
|
||||||
|
@ -24,13 +24,11 @@ class NotificationShown(
|
||||||
private const val COL_ACCT = "a"
|
private const val COL_ACCT = "a"
|
||||||
private const val COL_NOTIFICATION_ID = "ni"
|
private const val COL_NOTIFICATION_ID = "ni"
|
||||||
private const val COL_TIME_CREATE = "tc"
|
private const val COL_TIME_CREATE = "tc"
|
||||||
private const val COL_TIME_DISMISS = "td"
|
|
||||||
private val columnList = MetaColumns(table, initialVersion = 65).apply {
|
private val columnList = MetaColumns(table, initialVersion = 65).apply {
|
||||||
column(0, COL_ID, MetaColumns.TS_INT_PRIMARY_KEY_NOT_NULL)
|
column(0, COL_ID, MetaColumns.TS_INT_PRIMARY_KEY_NOT_NULL)
|
||||||
column(0, COL_ACCT, MetaColumns.TS_EMPTY_NOT_NULL)
|
column(0, COL_ACCT, MetaColumns.TS_EMPTY_NOT_NULL)
|
||||||
column(0, COL_NOTIFICATION_ID, 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_CREATE, MetaColumns.TS_ZERO_NOT_NULL)
|
||||||
column(0, COL_TIME_DISMISS, MetaColumns.TS_ZERO_NOT_NULL)
|
|
||||||
createExtra = {
|
createExtra = {
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"create unique index if not exists ${table}_a on $table($COL_ACCT,$COL_NOTIFICATION_ID)",
|
"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(
|
db.execSQL(
|
||||||
"delete from $table where $COL_ACCT=?",
|
"delete from $table where $COL_ACCT=?",
|
||||||
arrayOf(acct)
|
arrayOf(acct)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun duplicateOrPut(acct: String, notificationId: String): Boolean {
|
fun duplicateOrPut(acct: Acct, notificationId: String): Boolean {
|
||||||
try {
|
try {
|
||||||
|
// 有効なIDがない場合は重複排除しない
|
||||||
|
when (notificationId) {
|
||||||
|
"", EntityId.DEFAULT.toString() -> return false
|
||||||
|
}
|
||||||
|
|
||||||
db.rawQuery(
|
db.rawQuery(
|
||||||
"select $COL_ID from $table where $COL_ACCT=? and $COL_NOTIFICATION_ID=? limit 1",
|
"select $COL_ID from $table where $COL_ACCT=? and $COL_NOTIFICATION_ID=? limit 1",
|
||||||
arrayOf(acct, notificationId)
|
arrayOf(acct.ascii, notificationId)
|
||||||
)?.use {
|
)?.use {
|
||||||
if (it.count > 0) return true
|
if (it.count > 0) return true
|
||||||
}
|
}
|
||||||
ContentValues().apply {
|
ContentValues().apply {
|
||||||
put(COL_TIME_CREATE, System.currentTimeMillis())
|
put(COL_TIME_CREATE, System.currentTimeMillis())
|
||||||
put(COL_ACCT, acct)
|
put(COL_ACCT, acct.ascii)
|
||||||
put(COL_NOTIFICATION_ID, notificationId)
|
put(COL_NOTIFICATION_ID, notificationId)
|
||||||
}.replaceTo(db, table)
|
}.replaceTo(db, table)
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
|
@ -148,5 +151,23 @@ class NotificationShown(
|
||||||
return false
|
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
|
// DBの主ID
|
||||||
var id: Long = 0L,
|
var id: Long = 0L,
|
||||||
// 通知を受け取るアカウントのacct。通知のタイトルでもある
|
// 通知を受け取るアカウントのacct。通知のタイトルでもある
|
||||||
var loginAcct: String? = null,
|
var loginAcct: Acct? = null,
|
||||||
// 通知情報に含まれるタイムスタンプ
|
// 通知情報に含まれるタイムスタンプ
|
||||||
var timestamp: Long = System.currentTimeMillis(),
|
var timestamp: Long = System.currentTimeMillis(),
|
||||||
// 通知を受信/保存した時刻
|
// 通知を受信/保存した時刻
|
||||||
|
@ -151,7 +151,7 @@ data class PushMessage(
|
||||||
fun readRow(cursor: Cursor) =
|
fun readRow(cursor: Cursor) =
|
||||||
PushMessage(
|
PushMessage(
|
||||||
id = cursor.getLong(idxId),
|
id = cursor.getLong(idxId),
|
||||||
loginAcct = cursor.getStringOrNull(idxLoginAcct),
|
loginAcct = cursor.getStringOrNull(idxLoginAcct)?.let{Acct.parse(it)},
|
||||||
timestamp = cursor.getLong(idxTimestamp),
|
timestamp = cursor.getLong(idxTimestamp),
|
||||||
timeSave = cursor.getLong(idxTimeSave),
|
timeSave = cursor.getLong(idxTimeSave),
|
||||||
timeDismiss = cursor.getLong(idxTimeDismiss),
|
timeDismiss = cursor.getLong(idxTimeDismiss),
|
||||||
|
@ -183,7 +183,7 @@ data class PushMessage(
|
||||||
|
|
||||||
// ID以外のカラムをContentValuesに変換する
|
// ID以外のカラムをContentValuesに変換する
|
||||||
fun toContentValues() = ContentValues().apply {
|
fun toContentValues() = ContentValues().apply {
|
||||||
put(COL_LOGIN_ACCT, loginAcct)
|
put(COL_LOGIN_ACCT, loginAcct?.ascii)
|
||||||
put(COL_TIMESTAMP, timestamp)
|
put(COL_TIMESTAMP, timestamp)
|
||||||
put(COL_TIME_SAVE, timeSave)
|
put(COL_TIME_SAVE, timeSave)
|
||||||
put(COL_TIME_DISMISS, timeDismiss)
|
put(COL_TIME_DISMISS, timeDismiss)
|
||||||
|
@ -253,6 +253,18 @@ data class PushMessage(
|
||||||
db.queryAll(TABLE, "$COL_TIME_SAVE desc")
|
db.queryAll(TABLE, "$COL_TIME_SAVE desc")
|
||||||
?.use { ColIdx(it).readAll(it) }
|
?.use { ColIdx(it).readAll(it) }
|
||||||
?: emptyList()
|
?: 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()
|
override fun hashCode() = if (id == 0L) super.hashCode() else id.hashCode()
|
||||||
|
|
|
@ -115,9 +115,14 @@ class SavedAccount(
|
||||||
@JsonPropBoolean("notificationPullEnable", false)
|
@JsonPropBoolean("notificationPullEnable", false)
|
||||||
var notificationPullEnable by jsonDelegates.boolean
|
var notificationPullEnable by jsonDelegates.boolean
|
||||||
|
|
||||||
|
@JsonPropInt("notificationAccentColor", 0)
|
||||||
|
var notificationAccentColor by jsonDelegates.int
|
||||||
|
|
||||||
init {
|
init {
|
||||||
log.i("ctor acctArg $acctArg")
|
log.i("ctor acctArg $acctArg")
|
||||||
|
|
||||||
|
// acctArg はMastodonの生のやつで、ドメイン部分がない場合がある
|
||||||
|
// Acct.parse はHost部分がnullのacctになるかもしれない
|
||||||
val tmpAcct = Acct.parse(acctArg)
|
val tmpAcct = Acct.parse(acctArg)
|
||||||
this.username = tmpAcct.username
|
this.username = tmpAcct.username
|
||||||
if (username.isEmpty()) error("missing username in acct")
|
if (username.isEmpty()) error("missing username in acct")
|
||||||
|
@ -128,6 +133,7 @@ class SavedAccount(
|
||||||
this.apiHost = tmpApiHost ?: tmpApDomain ?: tmpAcct.host ?: error("missing apiHost")
|
this.apiHost = tmpApiHost ?: tmpApDomain ?: tmpAcct.host ?: error("missing apiHost")
|
||||||
this.apDomain = tmpApDomain ?: tmpApiHost ?: tmpAcct.host ?: error("missing apDomain")
|
this.apDomain = tmpApDomain ?: tmpApiHost ?: tmpAcct.host ?: error("missing apDomain")
|
||||||
|
|
||||||
|
// Full Acct
|
||||||
this.acct = tmpAcct.followHost(apDomain)
|
this.acct = tmpAcct.followHost(apDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -590,15 +596,24 @@ class SavedAccount(
|
||||||
fun loadAccountList() =
|
fun loadAccountList() =
|
||||||
ArrayList<SavedAccount>().also { result ->
|
ArrayList<SavedAccount>().also { result ->
|
||||||
try {
|
try {
|
||||||
db.query(
|
db.rawQuery("select * from $table", emptyArray()).use { cursor ->
|
||||||
table,
|
while (cursor.moveToNext()) {
|
||||||
null,
|
parse(lazyContext, cursor)?.let { result.add(it) }
|
||||||
null,
|
}
|
||||||
null,
|
}
|
||||||
null,
|
} catch (ex: Throwable) {
|
||||||
null,
|
log.e(ex, "loadAccountList failed.")
|
||||||
null
|
lazyContext.showToast(
|
||||||
).use { cursor ->
|
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()) {
|
while (cursor.moveToNext()) {
|
||||||
parse(lazyContext, cursor)?.let { result.add(it) }
|
parse(lazyContext, cursor)?.let { result.add(it) }
|
||||||
}
|
}
|
||||||
|
@ -892,7 +907,8 @@ class SavedAccount(
|
||||||
|
|
||||||
TootNotification.TYPE_STATUS_REFERENCE -> notificationStatusReference
|
TootNotification.TYPE_STATUS_REFERENCE -> notificationStatusReference
|
||||||
|
|
||||||
else -> false
|
// 未知の通知はオフらない
|
||||||
|
else -> true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getResizeConfig() =
|
fun getResizeConfig() =
|
||||||
|
|
|
@ -231,7 +231,7 @@ class CustomEmojiCache(
|
||||||
data = try {
|
data = try {
|
||||||
App1.getHttpCached(request.url)
|
App1.getHttpCached(request.url)
|
||||||
} catch (ex: Throwable) {
|
} catch (ex: Throwable) {
|
||||||
log.w(ex, "get failed. url=${request.url}")
|
log.w( "get failed. url=${request.url}")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
te = elapsedTime
|
te = elapsedTime
|
||||||
|
|
|
@ -224,15 +224,7 @@ class CustomEmojiLister(
|
||||||
builder.post(JsonObject().toRequestBody())
|
builder.post(JsonObject().toRequestBody())
|
||||||
}?.decodeJsonObject()
|
}?.decodeJsonObject()
|
||||||
?.jsonArray("emojis")
|
?.jsonArray("emojis")
|
||||||
?.let { emojis12 ->
|
?.let { parseList(it, CustomEmoji::decodeMisskey) }
|
||||||
parseList(emojis12) {
|
|
||||||
CustomEmoji.decodeMisskey(
|
|
||||||
accessInfo.apDomain,
|
|
||||||
accessInfo.apiHost,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// v13のemojisを読む
|
// v13のemojisを読む
|
||||||
suspend fun misskeyEmojis13(): List<CustomEmoji>? =
|
suspend fun misskeyEmojis13(): List<CustomEmoji>? =
|
||||||
|
@ -247,11 +239,7 @@ class CustomEmojiLister(
|
||||||
?.jsonArray("emojis")
|
?.jsonArray("emojis")
|
||||||
?.let { emojis13 ->
|
?.let { emojis13 ->
|
||||||
parseList(emojis13) {
|
parseList(emojis13) {
|
||||||
CustomEmoji.decodeMisskey13(
|
CustomEmoji.decodeMisskey13(accessInfo.apiHost, it)
|
||||||
accessInfo.apDomain,
|
|
||||||
accessInfo.apiHost,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,13 +249,7 @@ class CustomEmojiLister(
|
||||||
"https://$cacheKey/api/v1/custom_emojis",
|
"https://$cacheKey/api/v1/custom_emojis",
|
||||||
accessInfo = accessInfo
|
accessInfo = accessInfo
|
||||||
)?.let { data ->
|
)?.let { data ->
|
||||||
parseList(data.decodeJsonArray()) {
|
parseList(data.decodeJsonArray(), CustomEmoji::decodeMastodon)
|
||||||
CustomEmoji.decode(
|
|
||||||
accessInfo.apDomain,
|
|
||||||
accessInfo.apiHost,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val list = when {
|
val list = when {
|
||||||
|
|
|
@ -20,8 +20,8 @@ class DecodeOptions(
|
||||||
var decodeEmoji: Boolean = false,
|
var decodeEmoji: Boolean = false,
|
||||||
var attachmentList: ArrayList<TootAttachmentLike>? = null,
|
var attachmentList: ArrayList<TootAttachmentLike>? = null,
|
||||||
var linkTag: Any? = null,
|
var linkTag: Any? = null,
|
||||||
var emojiMapCustom: HashMap<String, CustomEmoji>? = null,
|
var emojiMapCustom: Map<String, CustomEmoji>? = null,
|
||||||
var emojiMapProfile: HashMap<String, NicoProfileEmoji>? = null,
|
var emojiMapProfile: Map<String, NicoProfileEmoji>? = null,
|
||||||
var highlightTrie: WordTrieTree? = null,
|
var highlightTrie: WordTrieTree? = null,
|
||||||
var unwrapEmojiImageTag: Boolean = false,
|
var unwrapEmojiImageTag: Boolean = false,
|
||||||
var enlargeCustomEmoji: Float = 1f,
|
var enlargeCustomEmoji: Float = 1f,
|
||||||
|
|
|
@ -469,35 +469,13 @@ object EmojiDecoder {
|
||||||
val useEmojioneShortcode = PrefB.bpEmojioneShortcode.value
|
val useEmojioneShortcode = PrefB.bpEmojioneShortcode.value
|
||||||
val disableEmojiAnimation = PrefB.bpDisableEmojiAnimation.value
|
val disableEmojiAnimation = PrefB.bpDisableEmojiAnimation.value
|
||||||
|
|
||||||
splitShortCode(s, callback = object : ShortCodeSplitterCallback {
|
// カスタム絵文字のアニメーション切り替え
|
||||||
override fun onString(part: String) {
|
|
||||||
builder.addUnicodeString(part)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onShortCode(prevCodePoint: Int, part: String, name: String) {
|
|
||||||
// フレニコのプロフ絵文字
|
|
||||||
if (emojiMapProfile != null && name.length >= 2 && name[0] == '@') {
|
|
||||||
val emojiProfile = emojiMapProfile[name] ?: emojiMapProfile[name.substring(1)]
|
|
||||||
if (emojiProfile != null) {
|
|
||||||
val url = emojiProfile.url
|
|
||||||
if (url.isNotEmpty()) {
|
|
||||||
builder.addNetworkEmojiSpan(part, url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// カスタム絵文字
|
|
||||||
fun CustomEmoji.customEmojiToUrl(): String = when {
|
fun CustomEmoji.customEmojiToUrl(): String = when {
|
||||||
disableEmojiAnimation && staticUrl?.isNotEmpty() == true ->
|
disableEmojiAnimation && staticUrl?.isNotEmpty() == true -> staticUrl
|
||||||
this.staticUrl
|
else -> this.url
|
||||||
else ->
|
|
||||||
this.url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findCustomEmojiUrl(): String? {
|
fun findEmojiMisskey13(name: String): String? {
|
||||||
val misskeyVersion = options.linkHelper?.misskeyVersion ?: 0
|
|
||||||
if (misskeyVersion >= 13) {
|
|
||||||
val cols = name.split("@", limit = 2)
|
val cols = name.split("@", limit = 2)
|
||||||
val apiHostAscii = options.linkHelper?.apiHost?.ascii
|
val apiHostAscii = options.linkHelper?.apiHost?.ascii
|
||||||
|
|
||||||
|
@ -505,7 +483,6 @@ object EmojiDecoder {
|
||||||
val userHost = cols.elementAtOrNull(1)
|
val userHost = cols.elementAtOrNull(1)
|
||||||
?: options.authorDomain?.apiHost?.ascii
|
?: options.authorDomain?.apiHost?.ascii
|
||||||
?: apiHostAscii
|
?: apiHostAscii
|
||||||
|
|
||||||
log.i(
|
log.i(
|
||||||
"decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${
|
"decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${
|
||||||
cols.elementAtOrNull(1)
|
cols.elementAtOrNull(1)
|
||||||
|
@ -524,16 +501,43 @@ object EmojiDecoder {
|
||||||
}@$userHost.webp"
|
}@$userHost.webp"
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
// 存在確認せずに絵文字プロキシのURLを返す
|
||||||
// 閲覧先サーバの絵文字を探す
|
// 閲覧先サーバの絵文字を探す
|
||||||
App1.custom_emoji_lister.getCachedEmoji(apiHostAscii, name)
|
App1.custom_emoji_lister.getCachedEmoji(apiHostAscii, name)
|
||||||
?.let { return it.customEmojiToUrl() }
|
?.let { return it.customEmojiToUrl() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
return emojiMapCustom?.get(name)?.customEmojiToUrl()
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
val url = findCustomEmojiUrl()
|
override fun onShortCode(prevCodePoint: Int, part: String, name: String) {
|
||||||
|
// フレニコのプロフ絵文字
|
||||||
|
if (emojiMapProfile != null && name.length >= 2 && name[0] == '@') {
|
||||||
|
val emojiProfile = emojiMapProfile[name] ?: emojiMapProfile[name.substring(1)]
|
||||||
|
if (emojiProfile != null) {
|
||||||
|
val url = emojiProfile.url
|
||||||
|
if (url.isNotEmpty()) {
|
||||||
|
builder.addNetworkEmojiSpan(part, url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val url = findCustomEmojiUrl(name)
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
builder.addNetworkEmojiSpan(part, url)
|
builder.addNetworkEmojiSpan(part, url)
|
||||||
return
|
return
|
||||||
|
|
|
@ -15,23 +15,27 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestManager
|
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.GlideUrl
|
||||||
import com.bumptech.glide.load.model.LazyHeaders
|
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.GifDrawable
|
||||||
import com.bumptech.glide.load.resource.gif.MyGifDrawable
|
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.ImageViewTarget
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import jp.juggler.subwaytooter.pref.PrefB
|
import jp.juggler.subwaytooter.pref.PrefB
|
||||||
import jp.juggler.util.data.clip
|
import jp.juggler.util.data.clip
|
||||||
|
import jp.juggler.util.data.notEmpty
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
class MyNetworkImageView : AppCompatImageView {
|
class MyNetworkImageView : AppCompatImageView {
|
||||||
|
|
||||||
companion object {
|
|
||||||
internal val log = LogCategory("MyNetworkImageView")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ロード中などに表示するDrawableのリソースID
|
// ロード中などに表示するDrawableのリソースID
|
||||||
private var mDefaultImage: Drawable? = null
|
private var mDefaultImage: Drawable? = null
|
||||||
|
|
||||||
|
@ -43,7 +47,7 @@ class MyNetworkImageView : AppCompatImageView {
|
||||||
|
|
||||||
// 表示したい画像のURL
|
// 表示したい画像のURL
|
||||||
private var mUrl: String? = null
|
private var mUrl: String? = null
|
||||||
private var mMayGif: Boolean = false
|
private var mMayAnime: Boolean = false
|
||||||
|
|
||||||
// 非同期処理のキャンセル
|
// 非同期処理のキャンセル
|
||||||
private var mTarget: Target<*>? = null
|
private var mTarget: Target<*>? = null
|
||||||
|
@ -76,20 +80,19 @@ class MyNetworkImageView : AppCompatImageView {
|
||||||
fun setImageUrl(
|
fun setImageUrl(
|
||||||
r: Float,
|
r: Float,
|
||||||
url: String?,
|
url: String?,
|
||||||
gifUrlArg: String? = null,
|
animeUrl: String? = null,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
mCornerRadius = r
|
mCornerRadius = r
|
||||||
|
if (PrefB.bpEnableGifAnimation.value) {
|
||||||
val gifUrl = if (PrefB.bpEnableGifAnimation.value) gifUrlArg else null
|
animeUrl?.notEmpty()?.let {
|
||||||
|
mUrl = it
|
||||||
if (gifUrl?.isNotEmpty() == true) {
|
mMayAnime = true
|
||||||
mUrl = gifUrl
|
loadImageIfNecessary()
|
||||||
mMayGif = true
|
return
|
||||||
} else {
|
|
||||||
mUrl = url
|
|
||||||
mMayGif = false
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
mUrl = url
|
||||||
|
mMayAnime = false
|
||||||
loadImageIfNecessary()
|
loadImageIfNecessary()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,13 +181,15 @@ class MyNetworkImageView : AppCompatImageView {
|
||||||
|
|
||||||
val glideUrl = GlideUrl(url, glideHeaders)
|
val glideUrl = GlideUrl(url, glideHeaders)
|
||||||
|
|
||||||
mTarget = if (mMayGif) {
|
mTarget = if (mMayAnime) {
|
||||||
getGlide()
|
getGlide()
|
||||||
?.load(glideUrl)
|
?.load(glideUrl)
|
||||||
|
?.listener(listener)
|
||||||
?.into(MyTargetGif(url))
|
?.into(MyTargetGif(url))
|
||||||
} else {
|
} else {
|
||||||
getGlide()
|
getGlide()
|
||||||
?.load(glideUrl)
|
?.load(glideUrl)
|
||||||
|
?.listener(listener)
|
||||||
?.into(MyTarget(url))
|
?.into(MyTarget(url))
|
||||||
}
|
}
|
||||||
} catch (ex: Throwable) {
|
} 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" />
|
android:layout_weight="1" />
|
||||||
</LinearLayout>
|
</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
|
<TextView
|
||||||
android:id="@+id/tvPushPolicyDesc"
|
android:id="@+id/tvPushPolicyDesc"
|
||||||
style="@style/setting_row_label"
|
style="@style/setting_row_label"
|
||||||
|
@ -631,7 +667,7 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/setting_row_label"
|
style="@style/setting_row_label"
|
||||||
android:text="@string/push_notification_use" />
|
android:text="@string/pull_notification_use" />
|
||||||
|
|
||||||
<LinearLayout style="@style/setting_row_form">
|
<LinearLayout style="@style/setting_row_form">
|
||||||
|
|
||||||
|
|
|
@ -1237,4 +1237,6 @@
|
||||||
<string name="pull_notification_use">定期的なプル通知チェックを使う</string>
|
<string name="pull_notification_use">定期的なプル通知チェックを使う</string>
|
||||||
<string name="push_subscription_exists_updateing">現在のプッシュ購読を更新しています…</string>
|
<string name="push_subscription_exists_updateing">現在のプッシュ購読を更新しています…</string>
|
||||||
<string name="manually_update">手動Manually update</string>
|
<string name="manually_update">手動Manually update</string>
|
||||||
|
<string name="notification_push_distributor_disabled">通知のプッシュ配送サービスが選択されていません</string>
|
||||||
|
<string name="notification_accent_color">通知のアクセント色</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -166,7 +166,8 @@
|
||||||
<color name="colorNotificationAccentReaction">#f5f233</color>
|
<color name="colorNotificationAccentReaction">#f5f233</color>
|
||||||
<color name="colorNotificationAccentReblog">#39e3d5</color>
|
<color name="colorNotificationAccentReblog">#39e3d5</color>
|
||||||
<color name="colorNotificationAccentReply">#ff3dbb</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="colorNotificationAccentStatus">#33f597</color>
|
||||||
<color name="colorNotificationAccentUnfollow">#9433f5</color>
|
<color name="colorNotificationAccentUnfollow">#9433f5</color>
|
||||||
<color name="colorNotificationAccentUnknown">#ae1aed</color>
|
<color name="colorNotificationAccentUnknown">#ae1aed</color>
|
||||||
|
|
|
@ -1253,4 +1253,6 @@
|
||||||
<string name="fedibird_capacities">Fedibird capacities</string>
|
<string name="fedibird_capacities">Fedibird capacities</string>
|
||||||
<string name="push_subscription_exists_updateing">Updating current push subscription…</string>
|
<string name="push_subscription_exists_updateing">Updating current push subscription…</string>
|
||||||
<string name="manually_update">Manually update</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>
|
</resources>
|
||||||
|
|
|
@ -101,6 +101,7 @@ dependencies {
|
||||||
// api "io.insert-koin:koin-androidx-navigation:$koinVersion"
|
// api "io.insert-koin:koin-androidx-navigation:$koinVersion"
|
||||||
// api "io.insert-koin:koin-androidx-compose:$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:glide:$glideVersion"
|
||||||
api "com.github.bumptech.glide:annotations:$glideVersion"
|
api "com.github.bumptech.glide:annotations:$glideVersion"
|
||||||
api("com.github.bumptech.glide:okhttp3-integration:$glideVersion") {
|
api("com.github.bumptech.glide:okhttp3-integration:$glideVersion") {
|
||||||
|
|
Loading…
Reference in New Issue