From 07daa2f7ef0a50fd3a133c746e152bf39323fcdc Mon Sep 17 00:00:00 2001 From: tateisu Date: Wed, 11 Jan 2023 01:40:26 +0900 Subject: [PATCH] =?UTF-8?q?(Misskey=2013)=E7=B5=B5=E6=96=87=E5=AD=97?= =?UTF-8?q?=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC=E3=81=A8=E6=8A=95=E7=A8=BF?= =?UTF-8?q?=E4=B8=AD=E3=81=AE=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=AB=E3=81=AA?= =?UTF-8?q?=E3=82=AB=E3=82=B9=E3=82=BF=E3=83=A0=E7=B5=B5=E6=96=87=E5=AD=97?= =?UTF-8?q?=E3=81=AE=E8=A1=A8=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/compiler.xml | 1 + .idea/gradle.xml | 1 + .idea/modules.xml | 4 + .../jp/juggler/subwaytooter/ActMediaViewer.kt | 5 +- .../main/java/jp/juggler/subwaytooter/App1.kt | 25 ++-- .../juggler/subwaytooter/api/TootApiClient.kt | 4 +- .../subwaytooter/api/entity/EntityUtil.kt | 53 +++++-- .../api/entity/MisskeyNoteUpdate.kt | 4 +- .../subwaytooter/api/entity/TootAccount.kt | 9 +- .../api/entity/TootAnnouncement.kt | 10 +- .../subwaytooter/api/entity/TootInstance.kt | 6 +- .../subwaytooter/api/entity/TootStatus.kt | 34 ++--- .../subwaytooter/appsetting/AppSettingItem.kt | 4 +- .../subwaytooter/dialog/EmojiPicker.kt | 7 +- .../drawable/MediaBackgroundDrawable.kt | 35 +++-- .../juggler/subwaytooter/emoji/EmojiBase.kt | 18 ++- .../streaming/StreamConnection.kt | 6 +- .../subwaytooter/util/CustomEmojiLister.kt | 135 +++++++++++------- .../juggler/subwaytooter/util/EmojiDecoder.kt | 7 + .../subwaytooter/util/VersionString.kt | 7 + icon_material_symbols/.gitignore | 1 + icon_material_symbols/build.gradle | 41 ++++++ icon_material_symbols/consumer-rules.pro | 0 icon_material_symbols/proguard-rules.pro | 21 +++ .../ExampleInstrumentedTest.kt | 24 ++++ .../src/main/AndroidManifest.xml | 4 + .../icon_material_symbols/ExampleUnitTest.kt | 17 +++ settings.gradle | 1 + 28 files changed, 365 insertions(+), 119 deletions(-) create mode 100644 icon_material_symbols/.gitignore create mode 100644 icon_material_symbols/build.gradle create mode 100644 icon_material_symbols/consumer-rules.pro create mode 100644 icon_material_symbols/proguard-rules.pro create mode 100644 icon_material_symbols/src/androidTest/java/jp/juggler/icon_material_symbols/ExampleInstrumentedTest.kt create mode 100644 icon_material_symbols/src/main/AndroidManifest.xml create mode 100644 icon_material_symbols/src/test/java/jp/juggler/icon_material_symbols/ExampleUnitTest.kt diff --git a/.idea/compiler.xml b/.idea/compiler.xml index e8373bc9..ec241aa3 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -7,6 +7,7 @@ + diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 8ff9ded7..3a58a452 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -16,6 +16,7 @@ diff --git a/.idea/modules.xml b/.idea/modules.xml index 8f2a1d1b..bfc9e86b 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -22,6 +22,10 @@ + + + + diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt index 2c161230..03de2034 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt @@ -1,6 +1,5 @@ package jp.juggler.subwaytooter -import android.Manifest import android.annotation.SuppressLint import android.app.DownloadManager import android.content.ClipData @@ -296,6 +295,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener { App1.initEdgeToEdge(this) views.pbvImage.background = MediaBackgroundDrawable( + context = views.root.context, tileStep = tileStep, kind = MediaBackgroundDrawable.Kind.fromIndex(PrefI.ipMediaBackground(this)) ) @@ -811,11 +811,12 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener { private fun mediaBackgroundDialog() { val ad = ActionsDialog() for (k in MediaBackgroundDrawable.Kind.values()) { + if (!k.isMediaBackground) continue ad.addAction(k.name) { val idx = k.toIndex() appPref.edit().put(PrefI.ipMediaBackground, idx).apply() - views.pbvImage.background = MediaBackgroundDrawable( + context = views.root.context, tileStep = tileStep, kind = k ) diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.kt b/app/src/main/java/jp/juggler/subwaytooter/App1.kt index 7879542d..c1d706fe 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.kt @@ -465,22 +465,29 @@ class App1 : Application() { suspend fun getHttpCachedString( url: String, accessInfo: SavedAccount? = null, + misskeyPost: Boolean = false, builderBlock: (Request.Builder) -> Unit = {}, ): String? { val response: Response try { - val request_builder = Request.Builder() - .url(url) - .cacheControl(CACHE_CONTROL) - - val access_token = accessInfo?.getAccessToken() - if (access_token?.isNotEmpty() == true) { - request_builder.header("Authorization", "Bearer $access_token") + val request_builder = when { + misskeyPost && accessInfo?.isMisskey == true -> + accessInfo.putMisskeyApiToken().toPostRequestBuilder() + .url(url) + .cacheControl(CACHE_CONTROL) + else -> + Request.Builder() + .url(url) + .cacheControl(CACHE_CONTROL) + .also { + val access_token = accessInfo?.getAccessToken() + if (access_token?.isNotEmpty() == true) { + it.header("Authorization", "Bearer $access_token") + } + } } - builderBlock(request_builder) - val call = ok_http_client2.newCall(request_builder.build()) response = call.await() } catch (ex: Throwable) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt index 28131a0f..b06a585d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -498,7 +498,6 @@ class TootApiClient( try { if (!sendRequest(result) { - val url = "https://${apiHost?.ascii}$path" requestBuilder.url(url) @@ -508,7 +507,8 @@ class TootApiClient( requestBuilder.build() .also { log.d("request: ${it.method} $url") } - }) return result + } + ) return result return parseJson(result) } finally { diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityUtil.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityUtil.kt index fd76055f..13248e0d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityUtil.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/EntityUtil.kt @@ -5,7 +5,6 @@ import jp.juggler.util.JsonArray import jp.juggler.util.JsonException import jp.juggler.util.JsonObject import jp.juggler.util.LogCategory -import java.util.HashMap object EntityUtil { val log = LogCategory("EntityUtil") @@ -122,8 +121,9 @@ inline fun parseMapOrNull( } inline fun parseMapOrNull( - factory: (host: Host, src: JsonObject) -> V, - host: Host, + factory: (apDomain: Host, apiHost: Host, src: JsonObject) -> V, + apDomain: Host, + apiHost: Host, src: JsonArray?, log: LogCategory = EntityUtil.log, ): HashMap? where V : Mappable { @@ -132,7 +132,7 @@ inline fun parseMapOrNull( if (size > 0) { val dst = HashMap() for (i in 0 until size) { - val item = parseItem(factory, host, src.jsonObject(i), log) + val item = parseItem(factory, apDomain, apiHost, src.jsonObject(i), log) if (item != null) dst[item.mapKey] = item } if (dst.isNotEmpty()) return dst @@ -183,6 +183,22 @@ inline fun parseItem( } } +inline fun parseItem( + factory: (p1: P1, p2: P2, src: JsonObject) -> T, + p1: P1, + p2: P2, + src: JsonObject?, + log: LogCategory = EntityUtil.log, +): T? { + if (src == null) return null + return try { + factory(p1, p2, src) + } catch (ex: Throwable) { + log.e(ex, "${T::class.simpleName} parse failed.") + null + } +} + inline fun parseItem( factory: (serviceType: ServiceType, src: JsonObject) -> T, serviceType: ServiceType, @@ -198,9 +214,9 @@ inline fun parseItem( } } -inline fun parseList( - factory: (parser: TootParser, src: JsonObject) -> T, - parser: TootParser, +inline fun parseListP1( + factory: (p1: P1, src: JsonObject) -> T, + p1: P1, src: JsonArray?, log: LogCategory = EntityUtil.log, ): ArrayList { @@ -210,7 +226,28 @@ inline fun parseList( if (src_length > 0) { dst.ensureCapacity(src_length) for (i in src.indices) { - val item = parseItem(factory, parser, src.jsonObject(i), log) + val item = parseItem(factory, p1, src.jsonObject(i), log) + if (item != null) dst.add(item) + } + } + } + return dst +} + +inline fun parseListP2( + factory: (p1: P1, p2: P2, src: JsonObject) -> T, + p1: P1, + p2: P2, + src: JsonArray?, + log: LogCategory = EntityUtil.log, +): ArrayList { + val dst = ArrayList() + if (src != null) { + val src_length = src.size + if (src_length > 0) { + dst.ensureCapacity(src_length) + for (i in src.indices) { + val item = parseItem(factory, p1, p2, src.jsonObject(i), log) if (item != null) dst.add(item) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/MisskeyNoteUpdate.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/MisskeyNoteUpdate.kt index e9261b37..4ba34069 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/MisskeyNoteUpdate.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/MisskeyNoteUpdate.kt @@ -4,7 +4,7 @@ import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.util.JsonObject import jp.juggler.util.LogCategory -class MisskeyNoteUpdate(apDomain: Host, src: JsonObject) { +class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) { companion object { private val log = LogCategory("MisskeyNoteUpdate") } @@ -37,7 +37,7 @@ class MisskeyNoteUpdate(apDomain: Host, src: JsonObject) { userId = EntityId.mayDefault(body2.string("userId")) emoji = body2.jsonObject("emoji")?.let { try { - CustomEmoji.decodeMisskey(apDomain, it) + CustomEmoji.decodeMisskey(apDomain, apiHost, it) } catch (ex: Throwable) { log.e(ex, "can't parse custom emoji.") null diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt index a6db5d5e..b7820b4a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt @@ -138,6 +138,7 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain { parseMapOrNull( CustomEmoji.decodeMisskey, parser.apDomain, + parser.apiHost, src.jsonArray("emojis") ) this.profile_emojis = null @@ -276,8 +277,12 @@ open class TootAccount(parser: TootParser, src: JsonObject) : HostAndDomain { else -> { // 絵文字データは先に読んでおく - this.custom_emojis = - parseMapOrNull(CustomEmoji.decode, parser.apDomain, src.jsonArray("emojis")) + this.custom_emojis = parseMapOrNull( + CustomEmoji.decode, + parser.apDomain, + parser.apiHost, + src.jsonArray("emojis") + ) this.profile_emojis = when (val o = src["profile_emojis"]) { is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, TootStatus.log) diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt index 03b5cd68..06552076 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt @@ -7,7 +7,6 @@ import jp.juggler.subwaytooter.util.DecodeOptions import jp.juggler.util.JsonObject import jp.juggler.util.LogCategory import jp.juggler.util.notEmpty -import java.util.* class TootAnnouncement(parser: TootParser, src: JsonObject) { @@ -44,8 +43,13 @@ class TootAnnouncement(parser: TootParser, src: JsonObject) { init { // 絵文字マップはすぐ後で使うので、最初の方で読んでおく - this.custom_emojis = - parseMapOrNull(CustomEmoji.decode, parser.apDomain, src.jsonArray("emojis"), log) + this.custom_emojis = parseMapOrNull( + CustomEmoji.decode, + parser.apDomain, + parser.apiHost, + src.jsonArray("emojis"), + log + ) this.tags = TootTag.parseListOrNull(parser, src.jsonArray("tags")) diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt index 4be7ac50..a2f9b2f7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt @@ -239,11 +239,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { val canUseReference: Boolean? get() = fedibird_capabilities?.contains("status_reference") - fun versionGE(check: VersionString): Boolean { - if (decoded_version.isEmpty || check.isEmpty) return false - val i = VersionString.compare(decoded_version, check) - return i >= 0 - } + fun versionGE(check: VersionString) = decoded_version.ge(check) companion object { private val log = LogCategory("TootInstance") diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt index efb628c7..af6adc04 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt @@ -238,13 +238,13 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { // お気に入りカラムなどではパース直後に変更することがある // 絵文字マップはすぐ後で使うので、最初の方で読んでおく - this.custom_emojis = - parseMapOrNull( - CustomEmoji.decodeMisskey, - parser.apDomain, - src.jsonArray("emojis"), - log - ) + this.custom_emojis = parseMapOrNull( + CustomEmoji.decodeMisskey, + parser.apDomain, + parser.apiHost, + src.jsonArray("emojis"), + log + ) this.profile_emojis = null val who = parser.account(src.jsonObject("user")) @@ -581,13 +581,13 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { this.created_at = src.string("created_at") // 絵文字マップはすぐ後で使うので、最初の方で読んでおく - this.custom_emojis = - parseMapOrNull( - CustomEmoji.decode, - parser.apDomain, - src.jsonArray("emojis"), - log - ) + this.custom_emojis = parseMapOrNull( + CustomEmoji.decode, + parser.apDomain, + parser.apiHost, + src.jsonArray("emojis"), + log + ) this.profile_emojis = when (val o = src["profile_emojis"]) { is JsonArray -> parseMapOrNull(::NicoProfileEmoji, o, log) @@ -809,9 +809,9 @@ class TootStatus(parser: TootParser, src: JsonObject) : TimelineItem() { } private fun mergeMentions( - mentions1: java.util.ArrayList?, - mentions2: java.util.ArrayList?, - ): java.util.ArrayList? { + mentions1: List?, + mentions2: List?, + ): ArrayList? { val size = (mentions1?.size ?: 0) + (mentions2?.size ?: 0) if (size == 0) return null val dst = ArrayList(size) diff --git a/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt b/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt index bf8d5175..86664142 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt @@ -466,7 +466,9 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett sw(PrefB.bpUseInternalMediaViewer, R.string.use_internal_media_viewer) spinner(PrefI.ipMediaBackground, R.string.background_pattern) { - MediaBackgroundDrawable.Kind.values().map { it.name } + MediaBackgroundDrawable.Kind.values() + .filter{it.isMediaBackground} + .map { it.name } } sw(PrefB.bpPriorLocalURL, R.string.prior_local_url_when_open_attachment) diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt b/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt index 03e34df4..0cb1618b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/EmojiPicker.kt @@ -23,6 +23,7 @@ import com.google.android.flexbox.FlexboxLayout import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.databinding.EmojiPickerDialogBinding +import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable import jp.juggler.subwaytooter.emoji.* import jp.juggler.subwaytooter.global.appPref import jp.juggler.subwaytooter.pref.PrefB @@ -574,7 +575,7 @@ private class EmojiPicker( category.createFiltered(keywordLower) .takeIf { it.items.isNotEmpty() } }.forEach { - if (selectedCategory == null) add(it) + if( it.category == EmojiCategory.Custom || selectedCategory == null) add(it) addAll(it.items) val mod = it.items.size % gridCols if (mod > 0) { @@ -743,8 +744,12 @@ private class EmojiPicker( views.etFilter.addTextChangedListener(textWatcher) + + showFiltered(null, null) + val density = activity.resources.displayMetrics.density + views.giGrid.intercept = { handleTouch(it, wasIntercept = false) } views.giGrid.touch = { handleTouch(it, wasIntercept = true) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/drawable/MediaBackgroundDrawable.kt b/app/src/main/java/jp/juggler/subwaytooter/drawable/MediaBackgroundDrawable.kt index ee00e7e7..5a4821be 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/drawable/MediaBackgroundDrawable.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/drawable/MediaBackgroundDrawable.kt @@ -1,22 +1,35 @@ package jp.juggler.subwaytooter.drawable +import android.content.Context import android.graphics.* import android.graphics.drawable.Drawable -import androidx.annotation.ColorInt +import jp.juggler.subwaytooter.R +import jp.juggler.util.attrColor import kotlin.math.min class MediaBackgroundDrawable( + private val context: Context, private val tileStep: Int, - private val kind: Kind + private val kind: Kind, ) : Drawable() { - enum class Kind(@ColorInt val c1: Int, @ColorInt val c2: Int = 0) { - Black(Color.BLACK), - BlackTile(Color.BLACK, Color.BLACK or 0x202020), - Grey(Color.BLACK or 0x787878), - GreyTile(Color.BLACK or 0x707070, Color.BLACK or 0x808080), - White(Color.WHITE), - WhiteTile(Color.WHITE, Color.BLACK or 0xe0e0e0), + enum class Kind( + val c1: Context.() -> Int, + val c2: Context.() -> Int, + val isMediaBackground: Boolean = true, + ) { + Black({ Color.BLACK }, { 0 }), + BlackTile({ Color.BLACK }, { Color.BLACK or 0x202020 }), + Grey({ Color.BLACK or 0x787878 }, { 0 }), + GreyTile({ Color.BLACK or 0x707070 }, { Color.BLACK or 0x808080 }), + White({ Color.WHITE }, { 0 }), + WhiteTile({ Color.WHITE }, { Color.BLACK or 0xe0e0e0 }), + + EmojiPickerBg( + { attrColor(R.attr.colorWindowBackground) }, + { attrColor(R.attr.colorTimeSmall) }, + isMediaBackground = false + ), ; @@ -51,8 +64,8 @@ class MediaBackgroundDrawable( override fun draw(canvas: Canvas) { val bounds = this.bounds - val c1 = kind.c1 - val c2 = kind.c2 + val c1 = kind.c1.invoke(context) + val c2 = kind.c2.invoke(context) if (c2 == 0) { paint.color = c1 canvas.drawRect(bounds, paint) diff --git a/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiBase.kt b/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiBase.kt index 8cdeb7f1..42ffe0a5 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiBase.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiBase.kt @@ -7,7 +7,6 @@ import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.util.JsonArray import jp.juggler.util.JsonObject import jp.juggler.util.notEmpty -import java.util.* sealed interface EmojiBase @@ -81,7 +80,7 @@ class CustomEmoji( companion object { - val decode: (Host, JsonObject) -> CustomEmoji = { apDomain, src -> + val decode: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src -> CustomEmoji( apDomain = apDomain, shortcode = src.stringOrThrow("shortcode"), @@ -92,7 +91,7 @@ class CustomEmoji( ) } - val decodeMisskey: (Host, JsonObject) -> CustomEmoji = { apDomain, src -> + val decodeMisskey: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src -> val url = src.string("url") ?: error("missing url") CustomEmoji( @@ -105,6 +104,19 @@ class CustomEmoji( ) } + val decodeMisskey13: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, apiHost, src -> + val name = src.string("name") ?: error("missing name") + val url = "https://${apiHost.ascii}/emoji/$name.webp" + CustomEmoji( + apDomain = apDomain, + shortcode = name, + url = url, + staticUrl = url, + aliases = parseAliases(src.jsonArray("aliases")), + category = src.string("category"), + ) + } + private fun parseAliases(src: JsonArray?): ArrayList? { var dst = null as ArrayList? if (src != null) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt b/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt index a0dded7d..35152e8c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt @@ -197,7 +197,11 @@ class StreamConnection( log.e("$name handleMisskeyMessage: noteUpdated body is null") return } - fireNoteUpdated(MisskeyNoteUpdate(acctGroup.account.apDomain, body), channelId) + fireNoteUpdated(MisskeyNoteUpdate( + acctGroup.account.apDomain, + acctGroup.account.apiHost, + body + ), channelId) } "notification" -> { diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt index e65e8b6c..b52e7082 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt @@ -4,7 +4,7 @@ import android.content.Context import android.os.Handler import android.os.SystemClock import jp.juggler.subwaytooter.App1 -import jp.juggler.subwaytooter.api.entity.parseList +import jp.juggler.subwaytooter.api.entity.parseListP2 import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.* @@ -35,6 +35,7 @@ class CustomEmojiLister( val key: String, var list: List, var listWithAliases: List, + var mapShortCode: Map, // ロードした時刻 var timeUpdate: Long = elapsedTime, // 参照された時刻 @@ -63,7 +64,12 @@ class CustomEmojiLister( // エラーキャッシュ internal val cacheError = ConcurrentHashMap() - private val cacheErrorItem = CacheItem("error", emptyList(), emptyList()) + private val cacheErrorItem = CacheItem( + key = "error", + list = emptyList(), + listWithAliases = emptyList(), + mapShortCode = emptyMap(), + ) // ロード要求 internal val queue = ConcurrentLinkedQueue() @@ -75,18 +81,21 @@ class CustomEmojiLister( cacheError.clear() } - private fun getCached(now: Long, accessInfo: SavedAccount): CacheItem? { - val host = accessInfo.apiHost.ascii + private fun getCached(now: Long, accessInfo: SavedAccount) = + getCached(now, accessInfo.apiHost.ascii) + + private fun getCached(now: Long, apiHostAscii: String?): CacheItem? { + apiHostAscii ?: return null // 成功キャッシュ - val item = cache[host] + val item = cache[apiHostAscii] if (item != null && now - item.timeUpdate <= ERROR_EXPIRE) { item.timeUsed = now return item } // エラーキャッシュ - val timeError = cacheError[host] + val timeError = cacheError[apiHostAscii] if (timeError != null && now < timeError + ERROR_EXPIRE) { return cacheErrorItem } @@ -143,6 +152,9 @@ class CustomEmojiLister( } } + fun getCachedEmoji(apiHostAscii: String?, shortcode: String): CustomEmoji? = + getCached(elapsedTime, apiHostAscii)?.mapShortCode?.get(shortcode) + private inner class Worker : WorkerBase() { override fun cancel() { @@ -186,39 +198,92 @@ class CustomEmojiLister( val accessInfo = request.accessInfo val cacheKey = accessInfo.apiHost.ascii - val data = if (accessInfo.isMisskey) { + + // v12のmetaからemojisをパース + suspend fun misskeyEmojis12(): List? = App1.getHttpCachedString( "https://$cacheKey/api/meta", accessInfo = accessInfo ) { builder -> builder.post(JsonObject().toRequestBody()) + }?.decodeJsonObject() + ?.jsonArray("emojis") + ?.let { emojis12 -> + parseListP2( + CustomEmoji.decodeMisskey, + accessInfo.apDomain, + accessInfo.apiHost, + emojis12, + ) + } + + // v13のemojisを読む + suspend fun misskeyEmojis13(): List? = + App1.getHttpCachedString( + "https://$cacheKey/api/emojis", + accessInfo = accessInfo, + misskeyPost = true, + ) { builder -> + builder.post(JsonObject().toRequestBody()) } - } else { + ?.decodeJsonObject() + ?.jsonArray("emojis") + ?.let { emojis13 -> + parseListP2( + CustomEmoji.decodeMisskey13, + accessInfo.apDomain, + accessInfo.apiHost, + emojis13, + ) + } + + // マストドンのカスタム絵文字一覧を読む + suspend fun mastodonEmojis() = App1.getHttpCachedString( "https://$cacheKey/api/v1/custom_emojis", accessInfo = accessInfo - ) - } - var list: List? = null - var listWithAlias: List? = null - if (data != null) { - val a = decodeEmojiList(data, accessInfo) - list = a - listWithAlias = makeListWithAlias(a) - } + )?.let { data -> + parseListP2( + CustomEmoji.decode, + accessInfo.apDomain, + accessInfo.apiHost, + data.decodeJsonArray() + ) + } + + val list = when { + accessInfo.isMastodon -> mastodonEmojis() + else -> misskeyEmojis12() ?: misskeyEmojis13() + }?.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode }) + + val listWithAlias = list?.let { srcList -> + ArrayList(srcList).apply { + for (item in srcList) { + item.aliases + ?.filter { !it.equals(item.shortcode, ignoreCase = true) } + ?.forEach { add(item.makeAlias(it)) } + } + } + }?.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode }) + return synchronized(cache) { val now = elapsedTime if (list == null || listWithAlias == null) { cacheError[cacheKey] = now error("can't load custom emoji for ${accessInfo.apiHost}") } else { + val mapShortCode = buildMap { + list.forEach { put(it.alias ?: it.shortcode, it) } + listWithAlias.forEach { put(it.alias ?: it.shortcode, it) } + } var item = cache[cacheKey] if (item == null) { - item = CacheItem(cacheKey, list, listWithAlias) + item = CacheItem(cacheKey, list, listWithAlias, mapShortCode) cache[cacheKey] = item } else { item.list = list item.listWithAliases = listWithAlias + item.mapShortCode = mapShortCode item.timeUpdate = now } item @@ -249,39 +314,5 @@ class CustomEmojiLister( if (++removed >= over) break } } - - private fun decodeEmojiList( - data: String, - accessInfo: SavedAccount, - ): List = - if (accessInfo.isMisskey) { - parseList( - CustomEmoji.decodeMisskey, - accessInfo.apDomain, - data.decodeJsonObject().jsonArray("emojis") - ) - } else { - parseList( - CustomEmoji.decode, - accessInfo.apDomain, - data.decodeJsonArray() - ) - }.apply { - sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.shortcode }) - } - - private fun makeListWithAlias( - list: List, - ) = ArrayList().apply { - addAll(list) - for (item in list) { - val aliases = item.aliases ?: continue - for (alias in aliases) { - if (alias.equals(item.shortcode, ignoreCase = true)) continue - add(item.makeAlias(alias)) - } - } - sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.alias ?: it.shortcode }) - } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.kt b/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.kt index a53ca88e..144a2557 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.kt @@ -1,10 +1,12 @@ package jp.juggler.subwaytooter.util import android.content.Context +import android.os.SystemClock import android.text.SpannableStringBuilder import android.text.Spanned import android.util.SparseBooleanArray import androidx.annotation.DrawableRes +import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.EmojiMap @@ -382,6 +384,11 @@ object EmojiDecoder { // カスタム絵文字 val emojiCustom = emojiMapCustom?.get(name) + ?: App1.custom_emoji_lister.getCachedEmoji( + options.linkHelper?.apiHost?.ascii, + name + ) + if (emojiCustom != null) { val url = when { PrefB.bpDisableEmojiAnimation() && emojiCustom.staticUrl?.isNotEmpty() == true -> emojiCustom.staticUrl diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/VersionString.kt b/app/src/main/java/jp/juggler/subwaytooter/util/VersionString.kt index 483f1992..aa1041d3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/VersionString.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/VersionString.kt @@ -161,4 +161,11 @@ class VersionString(src: String?) : Comparable { } } } + + // false if this is empty or argument is empty + // else, true is this is greater or equal argument version + fun ge(other: VersionString) = when { + this.isEmpty || other.isEmpty -> false + else -> this >= other + } } diff --git a/icon_material_symbols/.gitignore b/icon_material_symbols/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/icon_material_symbols/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/icon_material_symbols/build.gradle b/icon_material_symbols/build.gradle new file mode 100644 index 00000000..95d80c3c --- /dev/null +++ b/icon_material_symbols/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'jp.juggler.icon_material_symbols' + compileSdk 32 + + defaultConfig { + minSdk 26 + targetSdk 32 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.5.1' + implementation 'com.google.android.material:material:1.7.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.4' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' +} \ No newline at end of file diff --git a/icon_material_symbols/consumer-rules.pro b/icon_material_symbols/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/icon_material_symbols/proguard-rules.pro b/icon_material_symbols/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/icon_material_symbols/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/icon_material_symbols/src/androidTest/java/jp/juggler/icon_material_symbols/ExampleInstrumentedTest.kt b/icon_material_symbols/src/androidTest/java/jp/juggler/icon_material_symbols/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..652bf87e --- /dev/null +++ b/icon_material_symbols/src/androidTest/java/jp/juggler/icon_material_symbols/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package jp.juggler.icon_material_symbols + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("jp.juggler.icon_material_symbols.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/icon_material_symbols/src/main/AndroidManifest.xml b/icon_material_symbols/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/icon_material_symbols/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/icon_material_symbols/src/test/java/jp/juggler/icon_material_symbols/ExampleUnitTest.kt b/icon_material_symbols/src/test/java/jp/juggler/icon_material_symbols/ExampleUnitTest.kt new file mode 100644 index 00000000..2ac8d5db --- /dev/null +++ b/icon_material_symbols/src/test/java/jp/juggler/icon_material_symbols/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package jp.juggler.icon_material_symbols + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 7706b5a7..1a17f00e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ include ':app', ':sample_apng', ':apng_android', ':apng', ':colorpicker', ':emoji' +include ':icon_material_symbols'