From ae76b86ab6d3856e2242d9264643a7210b034125 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 10 Mar 2026 11:42:13 +0200 Subject: [PATCH 001/172] =?UTF-8?q?feat(search):=20add=20query=20engine=20?= =?UTF-8?q?=E2=80=94=20SearchQuery,=20QueryParser,=20QuerySerializer,=20Ki?= =?UTF-8?q?ndRegistry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of advanced search. Recursive descent parser for operators (from:, kind:, since:, until:, lang:, domain:, #tag, -exclude, OR), serializer for bidirectional sync, kind alias registry with pseudo-kind support. 68 unit tests all passing. Co-Authored-By: Claude Opus 4.6 --- .../amethyst/commons/search/KindRegistry.kt | 67 ++++ .../amethyst/commons/search/QueryParser.kt | 346 ++++++++++++++++++ .../commons/search/QuerySerializer.kt | 113 ++++++ .../amethyst/commons/search/SearchQuery.kt | 60 +++ .../commons/search/KindRegistryTest.kt | 103 ++++++ .../commons/search/QueryParserTest.kt | 284 ++++++++++++++ .../commons/search/QuerySerializerTest.kt | 177 +++++++++ 7 files changed, 1150 insertions(+) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchQuery.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistryTest.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParserTest.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializerTest.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt new file mode 100644 index 0000000000..2b3e921b7f --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +object KindRegistry { + val aliases: Map> = + mapOf( + "note" to listOf(TextNoteEvent.KIND), + "article" to listOf(LongTextNoteEvent.KIND), + "repost" to listOf(RepostEvent.KIND), + "profile" to listOf(MetadataEvent.KIND), + "channel" to listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND), + "live" to listOf(LiveActivitiesEvent.KIND), + "community" to listOf(CommunityDefinitionEvent.KIND), + "wiki" to listOf(WikiNoteEvent.KIND), + "classified" to listOf(ClassifiedsEvent.KIND), + "highlight" to listOf(HighlightEvent.KIND), + ) + + val pseudoKinds: Set = setOf("reply", "media") + + val presets: Map> = + mapOf( + "Notes" to listOf(TextNoteEvent.KIND), + "Articles" to listOf(LongTextNoteEvent.KIND), + "Media" to listOf(TextNoteEvent.KIND), + "Channels" to listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND), + "Communities" to listOf(CommunityDefinitionEvent.KIND), + "Wiki" to listOf(WikiNoteEvent.KIND), + ) + + fun resolve(alias: String): List? = aliases[alias.lowercase()] + + fun isPseudoKind(alias: String): Boolean = alias.lowercase() in pseudoKinds + + fun nameFor(kind: Int): String? = aliases.entries.find { kind in it.value }?.key +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt new file mode 100644 index 0000000000..13de19b7c0 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList + +sealed interface Token { + data class Operator( + val name: String, + val value: String, + val raw: String, + ) : Token + + data class Text( + val value: String, + ) : Token + + data object Or : Token + + data class Quoted( + val value: String, + val raw: String, + ) : Token + + data class Negation( + val term: String, + ) : Token + + data class Hashtag( + val tag: String, + ) : Token +} + +object QueryParser { + private val KNOWN_OPERATORS = setOf("from", "kind", "since", "until", "lang", "domain") + + fun parse(input: String): SearchQuery { + if (input.isBlank()) return SearchQuery.EMPTY + val tokens = tokenize(input) + return buildQuery(tokens) + } + + internal fun tokenize(input: String): List { + val tokens = mutableListOf() + var i = 0 + val len = input.length + + while (i < len) { + // Skip whitespace + if (input[i].isWhitespace()) { + i++ + continue + } + + // Quoted phrase + if (input[i] == '"') { + val start = i + i++ // skip opening quote + val sb = StringBuilder() + while (i < len && input[i] != '"') { + sb.append(input[i]) + i++ + } + if (i < len) i++ // skip closing quote + val value = sb.toString() + tokens.add(Token.Quoted(value, input.substring(start, i))) + continue + } + + // Negation + if (input[i] == '-' && i + 1 < len && !input[i + 1].isWhitespace()) { + i++ // skip - + val word = readWord(input, i) + i += word.length + if (word.isNotEmpty()) { + tokens.add(Token.Negation(word)) + } + continue + } + + // Hashtag + if (input[i] == '#' && i + 1 < len && !input[i + 1].isWhitespace()) { + i++ // skip # + val tag = readWord(input, i) + i += tag.length + if (tag.isNotEmpty()) { + tokens.add(Token.Hashtag(tag)) + } + continue + } + + // Read a word (may be operator:value, OR, or plain text) + val word = readWord(input, i) + i += word.length + + if (word.isEmpty()) { + i++ + continue + } + + // Check for OR keyword + if (word == "OR") { + tokens.add(Token.Or) + continue + } + + // Check for operator pattern (word:value) + val colonIdx = word.indexOf(':') + if (colonIdx > 0) { + val opName = word.substring(0, colonIdx).lowercase() + val opValue = word.substring(colonIdx + 1) + if (opName in KNOWN_OPERATORS && opValue.isNotEmpty()) { + tokens.add(Token.Operator(opName, opValue, word)) + continue + } + // Malformed operator (no value or unknown) → treat as text + } + + tokens.add(Token.Text(word)) + } + + return tokens + } + + private fun readWord( + input: String, + start: Int, + ): String { + var i = start + while (i < input.length && !input[i].isWhitespace()) { + i++ + } + return input.substring(start, i) + } + + private fun buildQuery(tokens: List): SearchQuery { + val authors = mutableListOf() + val authorNames = mutableListOf() + val kinds = mutableListOf() + val hashtags = mutableListOf() + val excludeTerms = mutableListOf() + val pseudoKinds = mutableListOf() + val textParts = mutableListOf() + val orTerms = mutableListOf() + var since: Long? = null + var until: Long? = null + var language: String? = null + var domain: String? = null + + // Collect OR groups: text terms separated by OR + var i = 0 + while (i < tokens.size) { + when (val token = tokens[i]) { + is Token.Operator -> { + when (token.name) { + "from" -> { + val hex = decodePublicKeyAsHexOrNull(token.value) + if (hex != null) { + authors.add(hex) + } else { + authorNames.add(token.value) + } + } + + "kind" -> { + if (KindRegistry.isPseudoKind(token.value)) { + pseudoKinds.add(token.value.lowercase()) + } else { + val resolved = KindRegistry.resolve(token.value) + if (resolved != null) { + kinds.addAll(resolved) + } else { + token.value.toIntOrNull()?.let { kinds.add(it) } + ?: textParts.add(token.raw) + } + } + } + + "since" -> { + val ts = parseDateToTimestamp(token.value) + if (ts != null) { + since = ts + } else { + textParts.add(token.raw) + } + } + + "until" -> { + val ts = parseDateToTimestamp(token.value) + if (ts != null) { + until = ts + } else { + textParts.add(token.raw) + } + } + + "lang" -> { + language = token.value.lowercase() + } + + "domain" -> { + domain = token.value.lowercase() + } + } + } + + is Token.Text -> { + // Check if this is part of an OR chain + if (i + 2 < tokens.size && tokens[i + 1] is Token.Or && tokens[i + 2] is Token.Text) { + // Start of OR chain: collect all terms + orTerms.add(token.value) + i++ // skip to OR + while (i < tokens.size && tokens[i] is Token.Or && i + 1 < tokens.size && tokens[i + 1] is Token.Text) { + i++ // skip OR + orTerms.add((tokens[i] as Token.Text).value) + i++ // skip text + } + continue + } else { + textParts.add(token.value) + } + } + + is Token.Quoted -> { + textParts.add(token.raw) + } + + is Token.Negation -> { + excludeTerms.add(token.term) + } + + is Token.Hashtag -> { + hashtags.add(token.tag) + } + + is Token.Or -> { + // Orphaned OR (no adjacent text terms) → treat as text + textParts.add("OR") + } + } + i++ + } + + // Cap OR terms at 3 + val cappedOrTerms = orTerms.take(3) + + return SearchQuery( + text = textParts.joinToString(" "), + authors = authors.distinct().toImmutableList(), + authorNames = authorNames.distinct().toImmutableList(), + kinds = kinds.distinct().toImmutableList(), + since = since, + until = until, + hashtags = hashtags.distinct().toImmutableList(), + excludeTerms = excludeTerms.distinct().toImmutableList(), + language = language, + domain = domain, + orTerms = cappedOrTerms.toPersistentList(), + pseudoKinds = pseudoKinds.distinct().toImmutableList(), + ) + } + + internal fun parseDateToTimestamp(dateStr: String): Long? { + // ISO 8601 formats: YYYY, YYYY-MM, YYYY-MM-DD + return try { + val parts = dateStr.split("-") + when (parts.size) { + 1 -> { + val year = parts[0].toIntOrNull() ?: return null + if (year < 1970 || year > 2100) return null + dateToUnix(year, 1, 1) + } + + 2 -> { + val year = parts[0].toIntOrNull() ?: return null + val month = parts[1].toIntOrNull() ?: return null + if (year < 1970 || year > 2100 || month < 1 || month > 12) return null + dateToUnix(year, month, 1) + } + + 3 -> { + val year = parts[0].toIntOrNull() ?: return null + val month = parts[1].toIntOrNull() ?: return null + val day = parts[2].toIntOrNull() ?: return null + if (year < 1970 || year > 2100 || month < 1 || month > 12 || day < 1 || day > 31) return null + dateToUnix(year, month, day) + } + + else -> { + null + } + } + } catch (_: Exception) { + null + } + } + + private fun dateToUnix( + year: Int, + month: Int, + day: Int, + ): Long { + // Simple UTC date to unix timestamp calculation + // Days from epoch (1970-01-01) to the given date + var totalDays = 0L + + // Add days for full years + for (y in 1970 until year) { + totalDays += if (isLeapYear(y)) 366 else 365 + } + + // Add days for full months in target year + val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + if (isLeapYear(year)) daysInMonth[2] = 29 + for (m in 1 until month) { + totalDays += daysInMonth[m] + } + + // Add remaining days + totalDays += (day - 1) + + return totalDays * 86400L + } + + private fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt new file mode 100644 index 0000000000..00be7d75ea --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import com.vitorpamplona.quartz.nip19Bech32.entities.NPub + +object QuerySerializer { + fun serialize(query: SearchQuery): String { + if (query.isEmpty) return "" + + val parts = mutableListOf() + + // Operators first + query.authors.forEach { hex -> + val npub = + try { + NPub.create(hex) + } catch (_: Exception) { + null + } + parts.add("from:${npub ?: hex}") + } + query.authorNames.forEach { name -> + parts.add("from:$name") + } + query.kinds.forEach { kind -> + val name = KindRegistry.nameFor(kind) + parts.add("kind:${name ?: kind}") + } + query.pseudoKinds.forEach { pseudo -> + parts.add("kind:$pseudo") + } + query.since?.let { ts -> + parts.add("since:${timestampToDate(ts)}") + } + query.until?.let { ts -> + parts.add("until:${timestampToDate(ts)}") + } + query.language?.let { lang -> + parts.add("lang:$lang") + } + query.domain?.let { dom -> + parts.add("domain:$dom") + } + + // Hashtags + query.hashtags.forEach { tag -> + parts.add("#$tag") + } + + // Free text + if (query.text.isNotBlank()) { + parts.add(query.text) + } + + // OR terms + if (query.orTerms.isNotEmpty()) { + parts.add(query.orTerms.joinToString(" OR ")) + } + + // Exclusions last + query.excludeTerms.forEach { term -> + parts.add("-$term") + } + + return parts.joinToString(" ") + } + + internal fun timestampToDate(timestamp: Long): String { + // Convert unix timestamp to YYYY-MM-DD + var remaining = timestamp + var year = 1970 + while (true) { + val daysInYear = if (isLeapYear(year)) 366L else 365L + val secondsInYear = daysInYear * 86400L + if (remaining < secondsInYear) break + remaining -= secondsInYear + year++ + } + + val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + if (isLeapYear(year)) daysInMonth[2] = 29 + + var dayOfYear = (remaining / 86400).toInt() + 1 + var month = 1 + while (month <= 12 && dayOfYear > daysInMonth[month]) { + dayOfYear -= daysInMonth[month] + month++ + } + + return "$year-${month.toString().padStart(2, '0')}-${dayOfYear.toString().padStart(2, '0')}" + } + + private fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchQuery.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchQuery.kt new file mode 100644 index 0000000000..fff17c1df5 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchQuery.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +data class SearchQuery( + val text: String = "", + val authors: ImmutableList = persistentListOf(), + val authorNames: ImmutableList = persistentListOf(), + val kinds: ImmutableList = persistentListOf(), + val since: Long? = null, + val until: Long? = null, + val hashtags: ImmutableList = persistentListOf(), + val excludeTerms: ImmutableList = persistentListOf(), + val language: String? = null, + val domain: String? = null, + val orTerms: ImmutableList = persistentListOf(), + val pseudoKinds: ImmutableList = persistentListOf(), +) { + val isEmpty + get() = + text.isBlank() && + authors.isEmpty() && + authorNames.isEmpty() && + kinds.isEmpty() && + since == null && + until == null && + hashtags.isEmpty() && + orTerms.isEmpty() && + excludeTerms.isEmpty() && + pseudoKinds.isEmpty() && + language == null && + domain == null + + companion object { + val EMPTY = SearchQuery() + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistryTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistryTest.kt new file mode 100644 index 0000000000..5a03ab61c8 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistryTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class KindRegistryTest { + @Test + fun resolveNote() { + assertEquals(listOf(1), KindRegistry.resolve("note")) + } + + @Test + fun resolveArticle() { + assertEquals(listOf(30023), KindRegistry.resolve("article")) + } + + @Test + fun resolveChannel() { + val kinds = KindRegistry.resolve("channel")!! + assertTrue(40 in kinds) + assertTrue(41 in kinds) + } + + @Test + fun resolveCaseInsensitive() { + assertEquals(KindRegistry.resolve("NOTE"), KindRegistry.resolve("note")) + } + + @Test + fun resolveUnknown() { + assertNull(KindRegistry.resolve("unknown")) + } + + @Test + fun isPseudoKindReply() { + assertTrue(KindRegistry.isPseudoKind("reply")) + assertTrue(KindRegistry.isPseudoKind("Reply")) + } + + @Test + fun isPseudoKindMedia() { + assertTrue(KindRegistry.isPseudoKind("media")) + } + + @Test + fun isNotPseudoKind() { + assertFalse(KindRegistry.isPseudoKind("note")) + assertFalse(KindRegistry.isPseudoKind("article")) + } + + @Test + fun nameForKind1() { + assertEquals("note", KindRegistry.nameFor(1)) + } + + @Test + fun nameForKind30023() { + assertEquals("article", KindRegistry.nameFor(30023)) + } + + @Test + fun nameForUnknownKind() { + assertNull(KindRegistry.nameFor(99999)) + } + + @Test + fun allAliasesResolve() { + KindRegistry.aliases.forEach { (alias, kinds) -> + assertEquals(kinds, KindRegistry.resolve(alias)) + } + } + + @Test + fun presetsContainExpectedEntries() { + assertTrue(KindRegistry.presets.containsKey("Notes")) + assertTrue(KindRegistry.presets.containsKey("Articles")) + assertTrue(KindRegistry.presets.containsKey("Media")) + assertTrue(KindRegistry.presets.containsKey("Channels")) + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParserTest.kt new file mode 100644 index 0000000000..4cae7f2c30 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParserTest.kt @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class QueryParserTest { + @Test + fun emptyInput() { + val q = QueryParser.parse("") + assertTrue(q.isEmpty) + assertEquals(SearchQuery.EMPTY, q) + } + + @Test + fun whitespaceOnly() { + val q = QueryParser.parse(" ") + assertTrue(q.isEmpty) + } + + @Test + fun plainText() { + val q = QueryParser.parse("bitcoin lightning") + assertEquals("bitcoin lightning", q.text) + assertTrue(q.authors.isEmpty()) + assertTrue(q.kinds.isEmpty()) + } + + @Test + fun kindOperatorAlias() { + val q = QueryParser.parse("kind:note") + assertEquals(listOf(1), q.kinds.toList()) + assertTrue(q.text.isBlank()) + } + + @Test + fun kindOperatorNumeric() { + val q = QueryParser.parse("kind:30023") + assertEquals(listOf(30023), q.kinds.toList()) + } + + @Test + fun kindOperatorArticle() { + val q = QueryParser.parse("kind:article") + assertEquals(listOf(30023), q.kinds.toList()) + } + + @Test + fun kindOperatorInvalid() { + val q = QueryParser.parse("kind:invalid") + // Unresolvable kind → treated as text + assertEquals("kind:invalid", q.text) + assertTrue(q.kinds.isEmpty()) + } + + @Test + fun pseudoKindReply() { + val q = QueryParser.parse("kind:reply") + assertTrue(q.kinds.isEmpty()) + assertEquals(listOf("reply"), q.pseudoKinds.toList()) + } + + @Test + fun pseudoKindMedia() { + val q = QueryParser.parse("kind:media") + assertTrue(q.kinds.isEmpty()) + assertEquals(listOf("media"), q.pseudoKinds.toList()) + } + + @Test + fun multipleKinds() { + val q = QueryParser.parse("kind:note kind:article") + assertEquals(listOf(1, 30023), q.kinds.toList()) + } + + @Test + fun sinceDate() { + val q = QueryParser.parse("since:2025-01-01") + // 2025-01-01 00:00:00 UTC + assertEquals(1735689600L, q.since) + } + + @Test + fun sinceDateYearOnly() { + val q = QueryParser.parse("since:2025") + // 2025-01-01 00:00:00 UTC + assertEquals(1735689600L, q.since) + } + + @Test + fun sinceDateYearMonth() { + val q = QueryParser.parse("since:2025-06") + // 2025-06-01 00:00:00 UTC + val q2 = QueryParser.parse("since:2025-06-01") + assertEquals(q2.since, q.since) + } + + @Test + fun sinceInvalidDate() { + val q = QueryParser.parse("since:not-a-date") + assertNull(q.since) + assertEquals("since:not-a-date", q.text) + } + + @Test + fun untilDate() { + val q = QueryParser.parse("until:2025-12-31") + assertNull(q.since) + assertTrue(q.until != null && q.until!! > 0) + } + + @Test + fun hashtag() { + val q = QueryParser.parse("#bitcoin") + assertEquals(listOf("bitcoin"), q.hashtags.toList()) + assertTrue(q.text.isBlank()) + } + + @Test + fun multipleHashtags() { + val q = QueryParser.parse("#bitcoin #nostr") + assertEquals(listOf("bitcoin", "nostr"), q.hashtags.toList()) + } + + @Test + fun negationTerm() { + val q = QueryParser.parse("-spam") + assertEquals(listOf("spam"), q.excludeTerms.toList()) + assertTrue(q.text.isBlank()) + } + + @Test + fun multipleNegations() { + val q = QueryParser.parse("-spam -scam") + assertEquals(listOf("spam", "scam"), q.excludeTerms.toList()) + } + + @Test + fun quotedPhrase() { + val q = QueryParser.parse("\"exact phrase\"") + assertEquals("\"exact phrase\"", q.text) + } + + @Test + fun orTerms() { + val q = QueryParser.parse("bitcoin OR lightning") + assertEquals(listOf("bitcoin", "lightning"), q.orTerms.toList()) + assertTrue(q.text.isBlank()) + } + + @Test + fun orTermsWithOperators() { + val q = QueryParser.parse("from:vitor bitcoin OR lightning kind:note") + assertEquals(listOf("bitcoin", "lightning"), q.orTerms.toList()) + assertEquals(listOf(1), q.kinds.toList()) + // from:vitor → authorNames since it's not a valid npub + assertEquals(listOf("vitor"), q.authorNames.toList()) + } + + @Test + fun orTermsCappedAtThree() { + val q = QueryParser.parse("a OR b OR c OR d OR e") + assertEquals(3, q.orTerms.size) + assertEquals(listOf("a", "b", "c"), q.orTerms.toList()) + } + + @Test + fun orphanedOr() { + val q = QueryParser.parse("OR") + assertEquals("OR", q.text) + assertTrue(q.orTerms.isEmpty()) + } + + @Test + fun languageOperator() { + val q = QueryParser.parse("lang:en bitcoin") + assertEquals("en", q.language) + assertEquals("bitcoin", q.text) + } + + @Test + fun domainOperator() { + val q = QueryParser.parse("domain:nostr.com bitcoin") + assertEquals("nostr.com", q.domain) + assertEquals("bitcoin", q.text) + } + + @Test + fun combinedQuery() { + val q = QueryParser.parse("kind:note since:2025-01-01 #bitcoin -spam lightning") + assertEquals(listOf(1), q.kinds.toList()) + assertEquals(1735689600L, q.since) + assertEquals(listOf("bitcoin"), q.hashtags.toList()) + assertEquals(listOf("spam"), q.excludeTerms.toList()) + assertEquals("lightning", q.text) + } + + @Test + fun caseInsensitiveOperators() { + val q = QueryParser.parse("FROM:vitor KIND:Note") + assertEquals(listOf("vitor"), q.authorNames.toList()) + assertEquals(listOf(1), q.kinds.toList()) + } + + @Test + fun operatorWithNoValue() { + val q = QueryParser.parse("from:") + // Malformed → treated as text + assertEquals("from:", q.text) + assertTrue(q.authors.isEmpty()) + } + + @Test + fun fromWithAuthorName() { + val q = QueryParser.parse("from:vitor") + assertEquals(listOf("vitor"), q.authorNames.toList()) + assertTrue(q.authors.isEmpty()) + } + + @Test + fun multipleFromAuthors() { + val q = QueryParser.parse("from:alice from:bob") + assertEquals(listOf("alice", "bob"), q.authorNames.toList()) + } + + @Test + fun duplicateAuthorsDeduped() { + val q = QueryParser.parse("from:vitor from:vitor") + assertEquals(1, q.authorNames.size) + } + + @Test + fun parseDateToTimestamp_validDates() { + // 1970-01-01 = 0 + assertEquals(0L, QueryParser.parseDateToTimestamp("1970-01-01")) + // 2000-01-01 + assertEquals(946684800L, QueryParser.parseDateToTimestamp("2000-01-01")) + } + + @Test + fun parseDateToTimestamp_invalidDates() { + assertNull(QueryParser.parseDateToTimestamp("not-a-date")) + assertNull(QueryParser.parseDateToTimestamp("1800-01-01")) + assertNull(QueryParser.parseDateToTimestamp("2025-13-01")) + assertNull(QueryParser.parseDateToTimestamp("2025-01-32")) + } + + @Test + fun unicodeInFreeText() { + val q = QueryParser.parse("bitcoin 日本語 🚀") + assertFalse(q.isEmpty) + assertTrue(q.text.contains("日本語")) + assertTrue(q.text.contains("🚀")) + } + + @Test + fun veryLongQuery() { + val longText = "word ".repeat(100).trim() + val q = QueryParser.parse(longText) + assertFalse(q.isEmpty) + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializerTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializerTest.kt new file mode 100644 index 0000000000..0534e50138 --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializerTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import kotlinx.collections.immutable.persistentListOf +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class QuerySerializerTest { + @Test + fun emptyQuery() { + assertEquals("", QuerySerializer.serialize(SearchQuery.EMPTY)) + } + + @Test + fun textOnly() { + val q = SearchQuery(text = "bitcoin") + assertEquals("bitcoin", QuerySerializer.serialize(q)) + } + + @Test + fun kindOnly() { + val q = SearchQuery(kinds = persistentListOf(1)) + assertEquals("kind:note", QuerySerializer.serialize(q)) + } + + @Test + fun kindUnknown() { + val q = SearchQuery(kinds = persistentListOf(99999)) + assertEquals("kind:99999", QuerySerializer.serialize(q)) + } + + @Test + fun multipleKinds() { + val q = SearchQuery(kinds = persistentListOf(1, 30023)) + assertEquals("kind:note kind:article", QuerySerializer.serialize(q)) + } + + @Test + fun pseudoKinds() { + val q = SearchQuery(pseudoKinds = persistentListOf("reply", "media")) + assertEquals("kind:reply kind:media", QuerySerializer.serialize(q)) + } + + @Test + fun sinceDate() { + val q = SearchQuery(since = 1735689600L) // 2025-01-01 + assertEquals("since:2025-01-01", QuerySerializer.serialize(q)) + } + + @Test + fun untilDate() { + val q = SearchQuery(until = 1735689600L) + assertEquals("until:2025-01-01", QuerySerializer.serialize(q)) + } + + @Test + fun hashtags() { + val q = SearchQuery(hashtags = persistentListOf("bitcoin", "nostr")) + assertEquals("#bitcoin #nostr", QuerySerializer.serialize(q)) + } + + @Test + fun excludeTerms() { + val q = SearchQuery(excludeTerms = persistentListOf("spam", "scam")) + assertEquals("-spam -scam", QuerySerializer.serialize(q)) + } + + @Test + fun orTerms() { + val q = SearchQuery(orTerms = persistentListOf("bitcoin", "lightning")) + assertEquals("bitcoin OR lightning", QuerySerializer.serialize(q)) + } + + @Test + fun language() { + val q = SearchQuery(language = "en", text = "bitcoin") + assertEquals("lang:en bitcoin", QuerySerializer.serialize(q)) + } + + @Test + fun domain() { + val q = SearchQuery(domain = "nostr.com", text = "hello") + assertEquals("domain:nostr.com hello", QuerySerializer.serialize(q)) + } + + @Test + fun authorNames() { + val q = SearchQuery(authorNames = persistentListOf("vitor")) + assertEquals("from:vitor", QuerySerializer.serialize(q)) + } + + @Test + fun combinedQuery() { + val q = + SearchQuery( + authorNames = persistentListOf("vitor"), + kinds = persistentListOf(1), + since = 1735689600L, + hashtags = persistentListOf("bitcoin"), + text = "lightning", + excludeTerms = persistentListOf("spam"), + ) + val result = QuerySerializer.serialize(q) + assertTrue(result.contains("from:vitor")) + assertTrue(result.contains("kind:note")) + assertTrue(result.contains("since:2025-01-01")) + assertTrue(result.contains("#bitcoin")) + assertTrue(result.contains("lightning")) + assertTrue(result.contains("-spam")) + } + + @Test + fun orderingOperatorsFirst() { + val q = + SearchQuery( + authorNames = persistentListOf("alice"), + kinds = persistentListOf(1), + hashtags = persistentListOf("nostr"), + text = "hello", + excludeTerms = persistentListOf("bad"), + ) + val result = QuerySerializer.serialize(q) + val fromIdx = result.indexOf("from:") + val kindIdx = result.indexOf("kind:") + val hashIdx = result.indexOf("#nostr") + val textIdx = result.indexOf("hello") + val excludeIdx = result.indexOf("-bad") + assertTrue(fromIdx < kindIdx) + assertTrue(kindIdx < hashIdx) + assertTrue(hashIdx < textIdx) + assertTrue(textIdx < excludeIdx) + } + + @Test + fun timestampToDateEpoch() { + assertEquals("1970-01-01", QuerySerializer.timestampToDate(0L)) + } + + @Test + fun timestampToDate2025() { + assertEquals("2025-01-01", QuerySerializer.timestampToDate(1735689600L)) + } + + @Test + fun roundtrip() { + // Parse a complex query, serialize, parse again — should produce equivalent SearchQuery + val input = "kind:note since:2025-01-01 #bitcoin lightning -spam" + val q1 = QueryParser.parse(input) + val serialized = QuerySerializer.serialize(q1) + val q2 = QueryParser.parse(serialized) + assertEquals(q1.kinds, q2.kinds) + assertEquals(q1.since, q2.since) + assertEquals(q1.hashtags, q2.hashtags) + assertEquals(q1.excludeTerms, q2.excludeTerms) + assertEquals(q1.text, q2.text) + } +} From c3a45822d507f5c28ccc0062bbdf2cec50d190ed Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 10 Mar 2026 11:43:48 +0200 Subject: [PATCH 002/172] feat(search): add SearchFilterFactory and SearchResultFilter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2: SearchFilterFactory converts SearchQuery → List with default 3-group kind splitting (Android parity), NIP-50 inline extensions, OR query support. SearchResultFilter handles client-side exclusions, reply/media pseudo-kind detection, dedup. Co-Authored-By: Claude Opus 4.6 --- .../commons/search/SearchResultFilter.kt | 74 ++++++++ .../subscriptions/SearchFilterFactory.kt | 163 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultFilter.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultFilter.kt new file mode 100644 index 0000000000..6cff2d8cf4 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultFilter.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import com.vitorpamplona.quartz.nip01Core.core.Event + +object SearchResultFilter { + fun filter( + events: List, + query: SearchQuery, + ): List { + var result = events + + // Dedup by event ID + result = result.distinctBy { it.id } + + // Exclusion terms (client-side) + if (query.excludeTerms.isNotEmpty()) { + result = + result.filter { event -> + query.excludeTerms.none { term -> + event.content.contains(term, ignoreCase = true) + } + } + } + + // Pseudo-kind: reply (kind 1 with e tag) + if ("reply" in query.pseudoKinds) { + result = result.filter { event -> isReply(event) } + } + + // Pseudo-kind: media (kind 1 with imeta tag or image URLs) + if ("media" in query.pseudoKinds) { + result = result.filter { event -> isMedia(event) } + } + + // Sort by createdAt descending + return result.sortedByDescending { it.createdAt } + } + + fun isReply(event: Event): Boolean = event.kind == 1 && event.tags.any { it.size >= 2 && it[0] == "e" } + + fun isMedia(event: Event): Boolean { + if (event.kind != 1) return false + // Check for imeta tag + if (event.tags.any { it.size >= 2 && it[0] == "imeta" }) return true + // Check for image/video URLs in content + return IMAGE_URL_PATTERN.containsMatchIn(event.content) + } + + private val IMAGE_URL_PATTERN = + Regex( + """https?://\S+\.(jpg|jpeg|png|gif|webp|svg|mp4|webm|mov)""", + RegexOption.IGNORE_CASE, + ) +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt new file mode 100644 index 0000000000..7aa491f714 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.subscriptions + +import com.vitorpamplona.amethyst.commons.search.SearchQuery +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent +import com.vitorpamplona.quartz.experimental.nns.NNSEvent +import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip22Comments.CommentEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent +import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent +import com.vitorpamplona.quartz.nip51Lists.PinListEvent +import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent +import com.vitorpamplona.quartz.nip88Polls.response.PollResponseEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +object SearchFilterFactory { + // Default kind groups (ported from Android SearchPostsByText) + private val defaultKindGroup1 = + listOf( + TextNoteEvent.KIND, + LongTextNoteEvent.KIND, + BadgeDefinitionEvent.KIND, + PeopleListEvent.KIND, + BookmarkListEvent.KIND, + AudioHeaderEvent.KIND, + AudioTrackEvent.KIND, + PinListEvent.KIND, + PollNoteEvent.KIND, + ChannelCreateEvent.KIND, + ) + + private val defaultKindGroup2 = + listOf( + ChannelMetadataEvent.KIND, + ClassifiedsEvent.KIND, + CommunityDefinitionEvent.KIND, + EmojiPackEvent.KIND, + HighlightEvent.KIND, + LiveActivitiesEvent.KIND, + PublicMessageEvent.KIND, + NNSEvent.KIND, + WikiNoteEvent.KIND, + CommentEvent.KIND, + ) + + private val defaultKindGroup3 = + listOf( + InteractiveStoryPrologueEvent.KIND, + InteractiveStorySceneEvent.KIND, + FollowListEvent.KIND, + NipTextEvent.KIND, + PollEvent.KIND, + PollResponseEvent.KIND, + ) + + fun createFilters( + query: SearchQuery, + limit: Int = 100, + ): List { + if (query.isEmpty) return emptyList() + + val searchString = buildSearchString(query) + val tags = buildTags(query) + val authors = query.authors.takeIf { it.isNotEmpty() } + + if (query.kinds.isNotEmpty()) { + // User specified kinds — single filter (no group splitting needed) + return listOf( + Filter( + kinds = query.kinds.toList(), + search = searchString, + authors = authors, + tags = tags, + since = query.since, + until = query.until, + limit = limit, + ), + ) + } + + // No kinds specified — use default 3-group search (Android parity) + return listOf(defaultKindGroup1, defaultKindGroup2, defaultKindGroup3).map { kindGroup -> + Filter( + kinds = kindGroup, + search = searchString, + authors = authors, + tags = tags, + since = query.since, + until = query.until, + limit = limit, + ) + } + } + + fun createOrFilters( + query: SearchQuery, + limit: Int = 100, + ): List> { + if (query.orTerms.isEmpty()) return listOf(createFilters(query, limit)) + + // Each OR term gets its own set of filters + return query.orTerms.take(3).map { term -> + val termQuery = query.copy(text = term, orTerms = kotlinx.collections.immutable.persistentListOf()) + createFilters(termQuery, limit) + } + } + + private fun buildSearchString(query: SearchQuery): String? { + val parts = mutableListOf() + + // Free text (exclude negation terms — those are client-side only) + if (query.text.isNotBlank()) { + parts.add(query.text) + } + + // NIP-50 inline extensions + query.language?.let { parts.add("language:$it") } + query.domain?.let { parts.add("domain:$it") } + + return parts.joinToString(" ").takeIf { it.isNotBlank() } + } + + private fun buildTags(query: SearchQuery): Map>? { + if (query.hashtags.isEmpty()) return null + return mapOf("t" to query.hashtags.toList()) + } +} From 8265d3f70a52f76112bcf537b78bc6d313e63b7d Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 10 Mar 2026 11:44:57 +0200 Subject: [PATCH 003/172] feat(search): add AdvancedSearchBarState with bidirectional sync Phase 3: State holder with MutableStateFlow as SSOT, sourceOfChange discriminator to prevent parse/serialize loops, debounced query for subscriptions, results management, author name resolution via cache autocomplete. Co-Authored-By: Claude Opus 4.6 --- .../commons/search/AdvancedSearchBarState.kt | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt new file mode 100644 index 0000000000..ba054a8011 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.quartz.nip01Core.core.Event +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.stateIn + +enum class ChangeSource { + TEXT, + FORM, + INIT, +} + +@OptIn(FlowPreview::class) +class AdvancedSearchBarState( + private val cache: ICacheProvider, + private val scope: CoroutineScope, + private val debounceMs: Long = 300L, +) { + private val _query = MutableStateFlow(SearchQuery.EMPTY) + val query: StateFlow = _query.asStateFlow() + + private var _changeSource: ChangeSource = ChangeSource.INIT + val changeSource get() = _changeSource + + private val _rawText = MutableStateFlow("") + val rawText: StateFlow = _rawText.asStateFlow() + + val displayText: StateFlow = + combine(_query, _rawText) { query, raw -> + if (_changeSource == ChangeSource.TEXT) { + raw + } else { + QuerySerializer.serialize(query) + } + }.stateIn(scope, SharingStarted.Eagerly, "") + + val debouncedQuery: StateFlow = + _query + .debounce(debounceMs) + .stateIn(scope, SharingStarted.Eagerly, SearchQuery.EMPTY) + + // People search results (from cache + relay) + private val _peopleResults = MutableStateFlow>(persistentListOf()) + val peopleResults: StateFlow> = _peopleResults.asStateFlow() + + // Note/event results (from relay subscriptions) + private val _noteResults = MutableStateFlow>(persistentListOf()) + val noteResults: StateFlow> = _noteResults.asStateFlow() + + private val _isSearching = MutableStateFlow(false) + val isSearching: StateFlow = _isSearching.asStateFlow() + + // Author name suggestions for autocomplete + private val _authorSuggestions = MutableStateFlow>(persistentListOf()) + val authorSuggestions: StateFlow> = _authorSuggestions.asStateFlow() + + // Expanded panel state + private val _panelExpanded = MutableStateFlow(false) + val panelExpanded: StateFlow = _panelExpanded.asStateFlow() + + // Text bar input + fun updateFromText(rawText: String) { + _changeSource = ChangeSource.TEXT + _rawText.value = rawText + _query.value = QueryParser.parse(rawText) + } + + // Form panel inputs + fun updateKinds(kinds: List) { + _changeSource = ChangeSource.FORM + _query.value = _query.value.copy(kinds = kinds.toImmutableList()) + } + + fun addAuthor(hexOrName: String) { + _changeSource = ChangeSource.FORM + val current = _query.value + val hex = + com.vitorpamplona.quartz.nip19Bech32 + .decodePublicKeyAsHexOrNull(hexOrName) + if (hex != null) { + if (hex !in current.authors) { + _query.value = current.copy(authors = (current.authors + hex).toImmutableList()) + } + } else { + if (hexOrName !in current.authorNames) { + _query.value = current.copy(authorNames = (current.authorNames + hexOrName).toImmutableList()) + } + } + } + + fun removeAuthor(hex: String) { + _changeSource = ChangeSource.FORM + val current = _query.value + _query.value = + current.copy( + authors = current.authors.filter { it != hex }.toImmutableList(), + authorNames = current.authorNames.filter { it != hex }.toImmutableList(), + ) + } + + fun updateDateRange( + since: Long?, + until: Long?, + ) { + _changeSource = ChangeSource.FORM + _query.value = _query.value.copy(since = since, until = until) + } + + fun addHashtag(tag: String) { + _changeSource = ChangeSource.FORM + val current = _query.value + val cleaned = tag.removePrefix("#") + if (cleaned !in current.hashtags) { + _query.value = current.copy(hashtags = (current.hashtags + cleaned).toImmutableList()) + } + } + + fun removeHashtag(tag: String) { + _changeSource = ChangeSource.FORM + val current = _query.value + _query.value = current.copy(hashtags = current.hashtags.filter { it != tag }.toImmutableList()) + } + + fun addExcludeTerm(term: String) { + _changeSource = ChangeSource.FORM + val current = _query.value + if (term !in current.excludeTerms) { + _query.value = current.copy(excludeTerms = (current.excludeTerms + term).toImmutableList()) + } + } + + fun removeExcludeTerm(term: String) { + _changeSource = ChangeSource.FORM + val current = _query.value + _query.value = current.copy(excludeTerms = current.excludeTerms.filter { it != term }.toImmutableList()) + } + + fun updateLanguage(lang: String?) { + _changeSource = ChangeSource.FORM + _query.value = _query.value.copy(language = lang) + } + + fun togglePanel() { + _panelExpanded.value = !_panelExpanded.value + } + + fun clearSearch() { + _changeSource = ChangeSource.INIT + _rawText.value = "" + _query.value = SearchQuery.EMPTY + _peopleResults.value = persistentListOf() + _noteResults.value = persistentListOf() + _isSearching.value = false + } + + // Results management (called from subscription callbacks) + fun startSearching() { + _isSearching.value = true + } + + fun stopSearching() { + _isSearching.value = false + } + + fun clearResults() { + _peopleResults.value = persistentListOf() + _noteResults.value = persistentListOf() + } + + fun addPeopleResult(user: User) { + val current = _peopleResults.value + if (current.none { it.pubkeyHex == user.pubkeyHex }) { + _peopleResults.value = (current + user).toImmutableList() + } + } + + fun addNoteResults(events: List) { + val current = _noteResults.value + val existingIds = current.map { it.id }.toSet() + val newEvents = events.filter { it.id !in existingIds } + if (newEvents.isNotEmpty()) { + _noteResults.value = (current + newEvents).sortedByDescending { it.createdAt }.toImmutableList() + } + } + + // Name resolution for autocomplete + @Suppress("UNCHECKED_CAST") + fun resolveAuthorName(name: String) { + if (name.length < 2) { + _authorSuggestions.value = persistentListOf() + return + } + val results = cache.findUsersStartingWith(name, 5) as List + _authorSuggestions.value = results.toImmutableList() + } +} From 55154dddb25bd2f5dcba575ec0742a11fa77fc21 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 10 Mar 2026 12:12:17 +0200 Subject: [PATCH 004/172] feat(search): add advanced search UI with panel, results list, and SearchScreen rewrite Phase 4: Desktop UI composables for advanced search. - Rewrite SearchScreen.kt to use AdvancedSearchBarState with TextFieldValue - Add AdvancedSearchPanel with kind presets, author chips, date range, hashtags, language - Add SearchResultsList with sticky headers for People/Notes/Articles/Other sections - Make QueryParser.parseDateToTimestamp and QuerySerializer.timestampToDate public Co-Authored-By: Claude Opus 4.6 --- .../amethyst/commons/search/QueryParser.kt | 2 +- .../commons/search/QuerySerializer.kt | 2 +- .../amethyst/desktop/ui/SearchScreen.kt | 465 +++++++++--------- .../desktop/ui/search/AdvancedSearchPanel.kt | 409 +++++++++++++++ .../desktop/ui/search/SearchResultsList.kt | 327 ++++++++++++ 5 files changed, 978 insertions(+), 227 deletions(-) create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt index 13de19b7c0..be1a93e34b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt @@ -280,7 +280,7 @@ object QueryParser { ) } - internal fun parseDateToTimestamp(dateStr: String): Long? { + fun parseDateToTimestamp(dateStr: String): Long? { // ISO 8601 formats: YYYY, YYYY-MM, YYYY-MM-DD return try { val parts = dateStr.split("-") diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt index 00be7d75ea..dd7b0612aa 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt @@ -84,7 +84,7 @@ object QuerySerializer { return parts.joinToString(" ") } - internal fun timestampToDate(timestamp: Long): String { + fun timestampToDate(timestamp: Long): String { // Convert unix timestamp to YYYY-MM-DD var remaining = timestamp var year = 1970 diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index cc4cee93b7..22ac6ffa95 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -20,6 +20,11 @@ */ package com.vitorpamplona.amethyst.desktop.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -31,8 +36,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward @@ -41,9 +44,9 @@ import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Tag +import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -59,17 +62,26 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState import com.vitorpamplona.amethyst.commons.search.SearchResult -import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard -import com.vitorpamplona.amethyst.commons.viewmodels.SearchBarState +import com.vitorpamplona.amethyst.commons.search.SearchResultFilter +import com.vitorpamplona.amethyst.commons.search.parseSearchInput import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator +import com.vitorpamplona.amethyst.desktop.subscriptions.SearchFilterFactory +import com.vitorpamplona.amethyst.desktop.subscriptions.SubscriptionConfig import com.vitorpamplona.amethyst.desktop.subscriptions.createMetadataSubscription import com.vitorpamplona.amethyst.desktop.subscriptions.createSearchPeopleSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.generateSubId import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription +import com.vitorpamplona.amethyst.desktop.ui.search.AdvancedSearchPanel +import com.vitorpamplona.amethyst.desktop.ui.search.SearchResultsList import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull @@ -85,66 +97,101 @@ fun SearchScreen( modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() - val searchState = remember { SearchBarState(localCache, scope) } + val state = remember { AdvancedSearchBarState(localCache, scope) } val focusRequester = remember { FocusRequester() } - // Pre-fill initial query (e.g., hashtag column) + // Pre-fill initial query LaunchedEffect(initialQuery) { if (initialQuery.isNotBlank()) { - searchState.updateSearchText(initialQuery) + state.updateFromText(initialQuery) } } + val relayStatuses by relayManager.relayStatuses.collectAsState() + val connectedRelays by relayManager.connectedRelays.collectAsState() + val displayText by state.displayText.collectAsState() + val query by state.query.collectAsState() + val debouncedQuery by state.debouncedQuery.collectAsState() + val panelExpanded by state.panelExpanded.collectAsState() + val isSearching by state.isSearching.collectAsState() + val peopleResults by state.peopleResults.collectAsState() + val noteResults by state.noteResults.collectAsState() - // Collect state from SearchBarState - val searchText by searchState.searchText.collectAsState() - val bech32Results by searchState.bech32Results.collectAsState() - val cachedUserResults by searchState.cachedUserResults.collectAsState() - val relaySearchResults by searchState.relaySearchResults.collectAsState() - val isSearchingRelays by searchState.isSearchingRelays.collectAsState() + // Bech32 parsing (immediate, no debounce) + val bech32Results = remember(displayText) { parseSearchInput(displayText) } - // NIP-50 relay search when local cache has few/no results - rememberSubscription(relayStatuses, searchText, cachedUserResults.size, relayManager = relayManager) { - val configuredRelays = relayStatuses.keys - if (configuredRelays.isEmpty()) return@rememberSubscription null - - // Only search relays if we have a real query and limited local results - if (searchState.shouldSearchRelays) { - searchState.startRelaySearch() - createSearchPeopleSubscription( - relays = configuredRelays, - searchQuery = searchText, - limit = 20, - onEvent = { event, _, _, _ -> - if (event is MetadataEvent) { - localCache.consumeMetadata(event) - val user = localCache.getUserIfExists(event.pubKey) - if (user != null) { - searchState.addRelaySearchResult(user) - } - } - }, - onEose = { _, _ -> - searchState.endRelaySearch() - }, - ) - } else { - null - } - } - - // Subscribe to metadata for searched users (to populate cache) - rememberSubscription(relayStatuses, searchText, relayManager = relayManager) { - val configuredRelays = relayStatuses.keys - if (configuredRelays.isEmpty() || searchText.length < 2) { + // NIP-50 people search subscription + rememberSubscription(connectedRelays, debouncedQuery, relayManager = relayManager) { + if (connectedRelays.isEmpty() || debouncedQuery.isEmpty) { return@rememberSubscription null } + // Skip if bech32 detected + if (bech32Results.isNotEmpty()) return@rememberSubscription null - // If it's a specific pubkey search, fetch that user's metadata - val pubkeyHex = decodePublicKeyAsHexOrNull(searchText) + state.startSearching() + state.clearResults() + + createSearchPeopleSubscription( + relays = connectedRelays, + searchQuery = + debouncedQuery.text.ifBlank { + com.vitorpamplona.amethyst.commons.search.QuerySerializer + .serialize(debouncedQuery) + }, + limit = 20, + onEvent = { event, _, _, _ -> + if (event is MetadataEvent) { + localCache.consumeMetadata(event) + @Suppress("UNCHECKED_CAST") + val user = localCache.getUserIfExists(event.pubKey) as? User + if (user != null) { + state.addPeopleResult(user) + } + } + }, + onEose = { _, _ -> + state.stopSearching() + }, + ) + } + + // NIP-50 advanced note search subscription (kinds beyond people) + rememberSubscription(connectedRelays, debouncedQuery, relayManager = relayManager) { + if (connectedRelays.isEmpty() || debouncedQuery.isEmpty) { + return@rememberSubscription null + } + if (bech32Results.isNotEmpty()) return@rememberSubscription null + + val filters = SearchFilterFactory.createFilters(debouncedQuery) + if (filters.isEmpty()) return@rememberSubscription null + + SubscriptionConfig( + subId = generateSubId("adv-search"), + filters = filters, + relays = connectedRelays, + onEvent = { event, _, _, _ -> + // Skip metadata events (handled by people search) + if (event.kind == 0) return@SubscriptionConfig + val filtered = SearchResultFilter.filter(listOf(event), debouncedQuery) + if (filtered.isNotEmpty()) { + state.addNoteResults(filtered) + } + }, + onEose = { _, _ -> + state.stopSearching() + }, + ) + } + + // Metadata subscription for bech32 pubkey lookups + rememberSubscription(connectedRelays, displayText, relayManager = relayManager) { + if (connectedRelays.isEmpty() || displayText.length < 2) { + return@rememberSubscription null + } + val pubkeyHex = decodePublicKeyAsHexOrNull(displayText) if (pubkeyHex != null) { createMetadataSubscription( - relays = configuredRelays, + relays = connectedRelays, pubKeyHex = pubkeyHex, onEvent = { event, _, _, _ -> if (event is MetadataEvent) { @@ -157,14 +204,12 @@ fun SearchScreen( } } - // Auto-focus the search field + // Auto-focus LaunchedEffect(Unit) { focusRequester.requestFocus() } - Column( - modifier = modifier.fillMaxSize(), - ) { + Column(modifier = modifier.fillMaxSize()) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -184,173 +229,157 @@ fun SearchScreen( Spacer(Modifier.height(16.dp)) - // Search input field - OutlinedTextField( - value = searchText, - onValueChange = { searchState.updateSearchText(it) }, - modifier = - Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - placeholder = { Text("Search by name, npub, nevent, or #hashtag") }, - leadingIcon = { - Icon( - Icons.Default.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - trailingIcon = { - if (searchText.isNotEmpty()) { - IconButton(onClick = { searchState.clearSearch() }) { - Icon( - Icons.Default.Clear, - contentDescription = "Clear", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) + // Search bar with advanced toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedTextField( + value = + TextFieldValue( + text = displayText, + selection = TextRange(displayText.length), + ), + onValueChange = { state.updateFromText(it.text) }, + modifier = + Modifier + .weight(1f) + .focusRequester(focusRequester), + placeholder = { Text("Search notes, people, tags... or use operators") }, + leadingIcon = { + Icon( + Icons.Default.Search, + contentDescription = "Search", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + trailingIcon = { + if (displayText.isNotEmpty()) { + IconButton(onClick = { state.clearSearch() }) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - } - }, - singleLine = true, - shape = RoundedCornerShape(12.dp), - ) + }, + singleLine = true, + shape = RoundedCornerShape(12.dp), + ) + IconButton(onClick = { state.togglePanel() }) { + Icon( + Icons.Default.Tune, + contentDescription = "Advanced Search", + tint = + if (panelExpanded) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + } + + // Expandable advanced panel + AnimatedVisibility( + visible = panelExpanded, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + ) { + AdvancedSearchPanel( + query = query, + onKindsChanged = { state.updateKinds(it) }, + onAuthorAdded = { state.addAuthor(it) }, + onAuthorRemoved = { state.removeAuthor(it) }, + onDateRangeChanged = { since, until -> state.updateDateRange(since, until) }, + onHashtagAdded = { state.addHashtag(it) }, + onHashtagRemoved = { state.removeHashtag(it) }, + onExcludeAdded = { state.addExcludeTerm(it) }, + onExcludeRemoved = { state.removeExcludeTerm(it) }, + onLanguageChanged = { state.updateLanguage(it) }, + onClear = { state.clearSearch() }, + modifier = Modifier.padding(top = 8.dp), + ) + } Spacer(Modifier.height(16.dp)) // Results - val hasResults = bech32Results.isNotEmpty() || cachedUserResults.isNotEmpty() || relaySearchResults.isNotEmpty() + val hasAnyResults = + bech32Results.isNotEmpty() || peopleResults.isNotEmpty() || noteResults.isNotEmpty() - if (!hasResults && searchText.isNotEmpty() && searchText.length >= 2 && !isSearchingRelays) { + if (bech32Results.isNotEmpty()) { + // Show bech32 results (exact lookup) + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + "Direct lookup", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 4.dp), + ) + bech32Results.forEach { result -> + SearchResultCard( + result = result, + onNavigateToProfile = onNavigateToProfile, + onNavigateToThread = onNavigateToThread, + onNavigateToHashtag = onNavigateToHashtag, + ) + } + } + } else if (hasAnyResults) { + SearchResultsList( + state = state, + onNavigateToProfile = onNavigateToProfile, + onNavigateToThread = onNavigateToThread, + ) + } else if (!debouncedQuery.isEmpty && !isSearching) { Text( - "No matches found. Try a name, npub, nevent, or #hashtag.", + "No results found. Try broader terms or fewer filters.", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium, ) - } else if (isSearchingRelays && !hasResults) { + } else if (isSearching) { Text( "Searching relays...", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium, ) - } else if (hasResults) { - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - // Bech32/hex results first - if (bech32Results.isNotEmpty()) { - item { - Text( - "Direct lookup", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 4.dp), - ) - } - items(bech32Results) { result -> - SearchResultCard( - result = result, - onNavigateToProfile = onNavigateToProfile, - onNavigateToThread = onNavigateToThread, - onNavigateToHashtag = onNavigateToHashtag, - ) - } - } - - // Cached user results - if (cachedUserResults.isNotEmpty()) { - if (bech32Results.isNotEmpty()) { - item { - HorizontalDivider(Modifier.padding(vertical = 8.dp)) - } - } - item { - Text( - "Cached users (${cachedUserResults.size})", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 4.dp), - ) - } - items(cachedUserResults, key = { "cached-${it.pubkeyHex}" }) { user -> - UserSearchCard( - user = user, - onClick = { onNavigateToProfile(user.pubkeyHex) }, - ) - } - } - - // Relay search results (NIP-50) - if (relaySearchResults.isNotEmpty()) { - if (bech32Results.isNotEmpty() || cachedUserResults.isNotEmpty()) { - item { - HorizontalDivider(Modifier.padding(vertical = 8.dp)) - } - } - item { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - "From relays (${relaySearchResults.size})", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 4.dp), - ) - if (isSearchingRelays) { - Text( - "searching...", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.primary, - ) - } - } - } - items(relaySearchResults, key = { "relay-${it.pubkeyHex}" }) { user -> - UserSearchCard( - user = user, - onClick = { onNavigateToProfile(user.pubkeyHex) }, - ) - } - } else if (isSearchingRelays && cachedUserResults.isEmpty()) { - item { - Text( - "Searching relays...", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 8.dp), - ) - } - } - } } else { - // Empty state - Column( - modifier = Modifier.fillMaxWidth().padding(top = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - "Search for users or notes", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(Modifier.height(8.dp)) - Text( - "Enter a name or Nostr identifier:", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - ) - Spacer(Modifier.height(16.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - SearchHint("vitor", "Search by name") - SearchHint("npub1...", "User profile") - SearchHint("note1...", "Single note") - SearchHint("nevent1...", "Note with metadata") - SearchHint("#hashtag", "Hashtag search") - } - } + // Empty state with operator hints + EmptySearchHints() + } + } +} + +@Composable +private fun EmptySearchHints() { + Column( + modifier = Modifier.fillMaxWidth().padding(top = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + "Search for users, notes, or content", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(8.dp)) + Text( + "Use operators to refine your search:", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + ) + Spacer(Modifier.height(16.dp)) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + SearchHint("from:npub1...", "Filter by author") + SearchHint("kind:article", "Long-form content") + SearchHint("since:2025-01", "After January 2025") + SearchHint("#bitcoin", "Hashtag search") + SearchHint("\"exact phrase\"", "Exact match") + SearchHint("bitcoin OR nostr", "Either term") + SearchHint("-spam", "Exclude term") + SearchHint("lang:en", "Language filter") } } } @@ -392,25 +421,11 @@ private fun SearchResultCard( .fillMaxWidth() .clickable { when (result) { - is SearchResult.UserResult -> { - onNavigateToProfile(result.pubKeyHex) - } - - is SearchResult.CachedUserResult -> { - onNavigateToProfile(result.user.pubkeyHex) - } - - is SearchResult.NoteResult -> { - onNavigateToThread(result.noteIdHex) - } - - is SearchResult.AddressResult -> { - onNavigateToThread("${result.kind}:${result.pubKeyHex}:${result.dTag}") - } - - is SearchResult.HashtagResult -> { - onNavigateToHashtag(result.hashtag) - } + is SearchResult.UserResult -> onNavigateToProfile(result.pubKeyHex) + is SearchResult.CachedUserResult -> onNavigateToProfile(result.user.pubkeyHex) + is SearchResult.NoteResult -> onNavigateToThread(result.noteIdHex) + is SearchResult.AddressResult -> onNavigateToThread("${result.kind}:${result.pubKeyHex}:${result.dTag}") + is SearchResult.HashtagResult -> onNavigateToHashtag(result.hashtag) } }, colors = diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt new file mode 100644 index 0000000000..33de9faec0 --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt @@ -0,0 +1,409 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.ui.search + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.AssistChip +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.search.KindRegistry +import com.vitorpamplona.amethyst.commons.search.SearchQuery + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun AdvancedSearchPanel( + query: SearchQuery, + onKindsChanged: (List) -> Unit, + onAuthorAdded: (String) -> Unit, + onAuthorRemoved: (String) -> Unit, + onDateRangeChanged: (Long?, Long?) -> Unit, + onHashtagAdded: (String) -> Unit, + onHashtagRemoved: (String) -> Unit, + onExcludeAdded: (String) -> Unit, + onExcludeRemoved: (String) -> Unit, + onLanguageChanged: (String?) -> Unit, + onClear: () -> Unit, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + ), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + // Content type presets + Text( + "Content Type", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + KindRegistry.presets.forEach { (name, kinds) -> + FilterChip( + selected = query.kinds.toList().containsAll(kinds), + onClick = { onKindsChanged(toggleKinds(query.kinds.toList(), kinds)) }, + label = { Text(name) }, + ) + } + } + + // Author field + AuthorInputField( + authors = query.authors.toList() + query.authorNames.toList(), + onAuthorAdded = onAuthorAdded, + onAuthorRemoved = onAuthorRemoved, + ) + + // Date range + DateRangeFields( + since = query.since, + until = query.until, + onChanged = onDateRangeChanged, + ) + + // Hashtags + ChipGroupWithInput( + label = "Tags", + items = query.hashtags.toList(), + prefix = "#", + placeholder = "Add tag...", + onAdd = onHashtagAdded, + onRemove = onHashtagRemoved, + ) + + // Exclude terms + ChipGroupWithInput( + label = "Exclude", + items = query.excludeTerms.toList(), + prefix = "-", + placeholder = "Exclude term...", + onAdd = onExcludeAdded, + onRemove = onExcludeRemoved, + ) + + // Language + LanguageSelector( + selected = query.language, + onChanged = onLanguageChanged, + ) + + // Clear button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + OutlinedButton(onClick = onClear) { + Text("Clear All") + } + } + } + } +} + +private fun toggleKinds( + current: List, + toggle: List, +): List = + if (current.containsAll(toggle)) { + current - toggle.toSet() + } else { + (current + toggle).distinct() + } + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun AuthorInputField( + authors: List, + onAuthorAdded: (String) -> Unit, + onAuthorRemoved: (String) -> Unit, +) { + Column { + Text( + "Author", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + if (authors.isNotEmpty()) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + authors.forEach { author -> + AssistChip( + onClick = { onAuthorRemoved(author) }, + label = { + Text( + if (author.length > 16) author.take(8) + "..." + author.takeLast(4) else author, + style = MaterialTheme.typography.bodySmall, + ) + }, + trailingIcon = { + Icon(Icons.Default.Close, contentDescription = "Remove", modifier = Modifier.size(14.dp)) + }, + ) + } + } + Spacer(Modifier.height(4.dp)) + } + var authorInput by remember { mutableStateOf("") } + OutlinedTextField( + value = authorInput, + onValueChange = { authorInput = it }, + modifier = + Modifier.fillMaxWidth().onKeyEvent { + if (it.key == Key.Enter && authorInput.isNotBlank()) { + onAuthorAdded(authorInput.trim()) + authorInput = "" + true + } else { + false + } + }, + placeholder = { Text("npub or name...") }, + singleLine = true, + trailingIcon = { + if (authorInput.isNotBlank()) { + IconButton(onClick = { + onAuthorAdded(authorInput.trim()) + authorInput = "" + }) { + Icon(Icons.Default.Add, contentDescription = "Add author") + } + } + }, + ) + } +} + +@Composable +private fun DateRangeFields( + since: Long?, + until: Long?, + onChanged: (Long?, Long?) -> Unit, +) { + var sinceText by remember(since) { + mutableStateOf( + since?.let { + com.vitorpamplona.amethyst.commons.search.QuerySerializer + .timestampToDate(it) + } ?: "", + ) + } + var untilText by remember(until) { + mutableStateOf( + until?.let { + com.vitorpamplona.amethyst.commons.search.QuerySerializer + .timestampToDate(it) + } ?: "", + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + "Since", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + OutlinedTextField( + value = sinceText, + onValueChange = { + sinceText = it + val ts = + com.vitorpamplona.amethyst.commons.search.QueryParser + .parseDateToTimestamp(it) + onChanged(ts, until) + }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("YYYY-MM-DD") }, + singleLine = true, + ) + } + Column(modifier = Modifier.weight(1f)) { + Text( + "Until", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + OutlinedTextField( + value = untilText, + onValueChange = { + untilText = it + val ts = + com.vitorpamplona.amethyst.commons.search.QueryParser + .parseDateToTimestamp(it) + onChanged(since, ts) + }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("YYYY-MM-DD") }, + singleLine = true, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ChipGroupWithInput( + label: String, + items: List, + prefix: String, + placeholder: String, + onAdd: (String) -> Unit, + onRemove: (String) -> Unit, +) { + Column { + Text( + label, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + items.forEach { item -> + AssistChip( + onClick = { onRemove(item) }, + label = { Text("$prefix$item", style = MaterialTheme.typography.bodySmall) }, + trailingIcon = { + Icon(Icons.Default.Close, contentDescription = "Remove", modifier = Modifier.size(14.dp)) + }, + ) + } + } + if (items.isNotEmpty()) Spacer(Modifier.height(4.dp)) + var inputText by remember { mutableStateOf("") } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + OutlinedTextField( + value = inputText, + onValueChange = { inputText = it }, + modifier = + Modifier.weight(1f).onKeyEvent { + if (it.key == Key.Enter && inputText.isNotBlank()) { + onAdd(inputText.trim()) + inputText = "" + true + } else { + false + } + }, + placeholder = { Text(placeholder) }, + singleLine = true, + ) + TextButton( + onClick = { + if (inputText.isNotBlank()) { + onAdd(inputText.trim()) + inputText = "" + } + }, + ) { + Text("Add") + } + } + } +} + +@Composable +private fun LanguageSelector( + selected: String?, + onChanged: (String?) -> Unit, +) { + val languages = + listOf( + null to "Any", + "en" to "English", + "es" to "Spanish", + "pt" to "Portuguese", + "ja" to "Japanese", + "zh" to "Chinese", + "de" to "German", + "fr" to "French", + ) + + Column { + Text( + "Language", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + languages.forEach { (code, name) -> + FilterChip( + selected = selected == code, + onClick = { onChanged(code) }, + label = { Text(name) }, + ) + } + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt new file mode 100644 index 0000000000..e26d6a462b --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.ui.search + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Article +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Forum +import androidx.compose.material.icons.filled.Person +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState +import com.vitorpamplona.amethyst.commons.search.KindRegistry +import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent + +@Composable +fun SearchResultsList( + state: AdvancedSearchBarState, + onNavigateToProfile: (String) -> Unit, + onNavigateToThread: (String) -> Unit, + modifier: Modifier = Modifier, + listState: LazyListState = rememberLazyListState(), +) { + val people by state.peopleResults.collectAsState() + val notes by state.noteResults.collectAsState() + val isSearching by state.isSearching.collectAsState() + + val hasResults = people.isNotEmpty() || notes.isNotEmpty() + + if (!hasResults && isSearching) { + Text( + "Searching relays...", + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodyMedium, + modifier = modifier.padding(vertical = 8.dp), + ) + return + } + + if (!hasResults) return + + // Group notes by kind + val textNotes = notes.filter { it.kind == 1 } + val articles = notes.filter { it.kind == LongTextNoteEvent.KIND } + val otherNotes = notes.filter { it.kind != 1 && it.kind != LongTextNoteEvent.KIND } + + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier, + ) { + // People section + if (people.isNotEmpty()) { + stickyHeader(key = "header-people") { + SectionHeader("People", people.size, Icons.Default.Person) + } + val displayPeople = people.take(5) + items(displayPeople, key = { "person-${it.pubkeyHex}" }) { user -> + UserSearchCard( + user = user, + onClick = { onNavigateToProfile(user.pubkeyHex) }, + ) + } + if (people.size > 5) { + item(key = "people-expand") { + ExpandableSection( + remaining = people.drop(5), + key = { "person-more-${it.pubkeyHex}" }, + ) { user -> + UserSearchCard( + user = user, + onClick = { onNavigateToProfile(user.pubkeyHex) }, + ) + } + } + } + } + + // Notes section + if (textNotes.isNotEmpty()) { + if (people.isNotEmpty()) { + item(key = "divider-notes") { HorizontalDivider(Modifier.padding(vertical = 4.dp)) } + } + stickyHeader(key = "header-notes") { + SectionHeader("Notes", textNotes.size, Icons.Default.Description) + } + val displayNotes = textNotes.take(5) + items(displayNotes, key = { "note-${it.id}" }) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } + if (textNotes.size > 5) { + item(key = "notes-expand") { + ExpandableSection( + remaining = textNotes.drop(5), + key = { "note-more-${it.id}" }, + ) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } + } + } + } + + // Articles section + if (articles.isNotEmpty()) { + if (people.isNotEmpty() || textNotes.isNotEmpty()) { + item(key = "divider-articles") { HorizontalDivider(Modifier.padding(vertical = 4.dp)) } + } + stickyHeader(key = "header-articles") { + SectionHeader("Articles", articles.size, Icons.Default.Article) + } + items(articles.take(5), key = { "article-${it.id}" }) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } + if (articles.size > 5) { + item(key = "articles-expand") { + ExpandableSection( + remaining = articles.drop(5), + key = { "article-more-${it.id}" }, + ) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } + } + } + } + + // Other section + if (otherNotes.isNotEmpty()) { + item(key = "divider-other") { HorizontalDivider(Modifier.padding(vertical = 4.dp)) } + stickyHeader(key = "header-other") { + SectionHeader("Other", otherNotes.size, Icons.Default.Forum) + } + items(otherNotes.take(5), key = { "other-${it.id}" }) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } + } + + // Bottom padding + item(key = "bottom-spacer") { Spacer(Modifier.height(16.dp)) } + } +} + +@Composable +private fun SectionHeader( + title: String, + count: Int, + icon: ImageVector, +) { + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(vertical = 8.dp), + ) { + Icon( + icon, + contentDescription = null, + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + "$title ($count)", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun NotePreviewCard( + event: Event, + onClick: () -> Unit, +) { + Card( + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + ), + ) { + Column(modifier = Modifier.padding(12.dp)) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + // Kind badge + val kindName = KindRegistry.nameFor(event.kind) ?: "kind ${event.kind}" + Text( + kindName, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + ) + // Author (hex truncated) + Text( + event.pubKey.take(8) + "..." + event.pubKey.takeLast(4), + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.weight(1f)) + // Timestamp + Text( + formatTimestamp(event.createdAt), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(Modifier.height(4.dp)) + // Content preview + Text( + event.content.take(200), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +private fun ExpandableSection( + remaining: List, + key: (T) -> String, + content: @Composable (T) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + if (expanded) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + remaining.forEach { item -> + content(item) + } + } + } else { + TextButton( + onClick = { expanded = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Icon(Icons.Default.ExpandMore, contentDescription = null, modifier = Modifier.size(16.dp)) + Text("Show all ${remaining.size} more") + } + } +} + +private fun formatTimestamp(ts: Long): String { + val now = System.currentTimeMillis() / 1000 + val diff = now - ts + return when { + diff < 60 -> { + "just now" + } + + diff < 3600 -> { + "${diff / 60}m ago" + } + + diff < 86400 -> { + "${diff / 3600}h ago" + } + + diff < 604800 -> { + "${diff / 86400}d ago" + } + + else -> { + // Simple date format + val days = ts / 86400 + val year = 1970 + (days / 365).toInt() + "$year" + } + } +} From e1abb037be8d5cf99e17fdaf5ec08c5b5c240ddf Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 10 Mar 2026 12:13:34 +0200 Subject: [PATCH 005/172] feat(search): add SearchHistoryStore with recent queries and saved searches Phase 5: Persistent search history using Java Preferences API. - Last 20 queries stored, deduped by serialized text - Saved searches with labels and timestamps - Uses QuerySerializer/QueryParser for round-trip serialization (no new deps) Co-Authored-By: Claude Opus 4.6 --- .../amethyst/desktop/SearchHistoryStore.kt | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt new file mode 100644 index 0000000000..8271c1e67f --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop + +import com.vitorpamplona.amethyst.commons.search.QueryParser +import com.vitorpamplona.amethyst.commons.search.QuerySerializer +import com.vitorpamplona.amethyst.commons.search.SearchQuery +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.util.prefs.Preferences + +object SearchHistoryStore { + private val prefs: Preferences = Preferences.userNodeForPackage(SearchHistoryStore::class.java) + + private const val KEY_HISTORY = "search_history" + private const val KEY_SAVED = "saved_searches" + private const val SEPARATOR = "\n" + private const val SAVED_SEPARATOR = "\t" + private const val MAX_HISTORY = 20 + + private val _history = MutableStateFlow>(emptyList()) + val history: StateFlow> = _history.asStateFlow() + + private val _savedSearches = MutableStateFlow>(emptyList()) + val savedSearches: StateFlow> = _savedSearches.asStateFlow() + + init { + _history.value = loadHistory() + _savedSearches.value = loadSaved() + } + + fun addToHistory(query: SearchQuery) { + if (query.isEmpty) return + val serialized = QuerySerializer.serialize(query) + val current = _history.value.toMutableList() + current.removeAll { QuerySerializer.serialize(it) == serialized } + current.add(0, query) + if (current.size > MAX_HISTORY) { + current.subList(MAX_HISTORY, current.size).clear() + } + _history.value = current.toList() + persistHistory(current) + } + + fun clearHistory() { + _history.value = emptyList() + prefs.remove(KEY_HISTORY) + } + + fun saveSearch( + query: SearchQuery, + label: String, + ) { + if (query.isEmpty) return + val saved = + SavedSearch( + id = System.currentTimeMillis().toString(), + label = label, + query = query, + createdAt = System.currentTimeMillis() / 1000, + ) + val current = _savedSearches.value + saved + _savedSearches.value = current + persistSaved(current) + } + + fun deleteSavedSearch(id: String) { + val current = _savedSearches.value.filter { it.id != id } + _savedSearches.value = current + persistSaved(current) + } + + private fun loadHistory(): List { + val raw = prefs.get(KEY_HISTORY, "") + if (raw.isBlank()) return emptyList() + return raw + .split(SEPARATOR) + .filter { it.isNotBlank() } + .mapNotNull { line -> + val parsed = QueryParser.parse(line) + if (parsed.isEmpty) null else parsed + } + } + + private fun persistHistory(queries: List) { + val raw = queries.joinToString(SEPARATOR) { QuerySerializer.serialize(it) } + prefs.put(KEY_HISTORY, raw) + } + + private fun loadSaved(): List { + val raw = prefs.get(KEY_SAVED, "") + if (raw.isBlank()) return emptyList() + return raw + .split(SEPARATOR) + .filter { it.isNotBlank() } + .mapNotNull { line -> + val parts = line.split(SAVED_SEPARATOR) + if (parts.size < 4) return@mapNotNull null + val id = parts[0] + val label = parts[1] + val createdAt = parts[2].toLongOrNull() ?: return@mapNotNull null + val queryText = parts[3] + val query = QueryParser.parse(queryText) + if (query.isEmpty) return@mapNotNull null + SavedSearch(id = id, label = label, query = query, createdAt = createdAt) + } + } + + private fun persistSaved(searches: List) { + val raw = + searches.joinToString(SEPARATOR) { s -> + listOf(s.id, s.label, s.createdAt.toString(), QuerySerializer.serialize(s.query)) + .joinToString(SAVED_SEPARATOR) + } + prefs.put(KEY_SAVED, raw) + } +} + +data class SavedSearch( + val id: String, + val label: String, + val query: SearchQuery, + val createdAt: Long, +) From b3952ce9db677c0ee0dc7ed47f8e345d2a41aaab Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Tue, 10 Mar 2026 12:15:14 +0200 Subject: [PATCH 006/172] feat(search): add keyboard shortcuts, search history UI, and operator hints Phase 6: Integration and polish. - Escape key closes advanced panel or clears search - Search history shown in empty state (recent + saved searches) - Auto-save to history when search completes - Updated empty state with operator hint examples Co-Authored-By: Claude Opus 4.6 --- .../amethyst/desktop/ui/SearchScreen.kt | 212 +++++++++++++++--- 1 file changed, 184 insertions(+), 28 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index 22ac6ffa95..d7fd9a6439 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -36,22 +36,29 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.filled.Tag import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -62,15 +69,22 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState +import com.vitorpamplona.amethyst.commons.search.QuerySerializer import com.vitorpamplona.amethyst.commons.search.SearchResult import com.vitorpamplona.amethyst.commons.search.SearchResultFilter import com.vitorpamplona.amethyst.commons.search.parseSearchInput +import com.vitorpamplona.amethyst.desktop.SearchHistoryStore import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator @@ -204,12 +218,44 @@ fun SearchScreen( } } + // Save to history when search completes + LaunchedEffect(isSearching, debouncedQuery) { + if (!isSearching && !debouncedQuery.isEmpty) { + SearchHistoryStore.addToHistory(debouncedQuery) + } + } + + // History state + val historyItems by SearchHistoryStore.history.collectAsState() + val savedSearches by SearchHistoryStore.savedSearches.collectAsState() + // Auto-focus LaunchedEffect(Unit) { focusRequester.requestFocus() } - Column(modifier = modifier.fillMaxSize()) { + Column( + modifier = + modifier + .fillMaxSize() + .onPreviewKeyEvent { event -> + if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false + when (event.key) { + Key.Escape -> { + if (panelExpanded) { + state.togglePanel() + } else if (displayText.isNotEmpty()) { + state.clearSearch() + } + true + } + + else -> { + false + } + } + }, + ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -347,40 +393,150 @@ fun SearchScreen( style = MaterialTheme.typography.bodyMedium, ) } else { - // Empty state with operator hints - EmptySearchHints() + // Empty state: show history + saved searches + operator hints + SearchEmptyState( + historyItems = historyItems, + savedSearches = savedSearches, + onLoadQuery = { query -> state.updateFromText(QuerySerializer.serialize(query)) }, + onDeleteSaved = { id -> SearchHistoryStore.deleteSavedSearch(id) }, + onClearHistory = { SearchHistoryStore.clearHistory() }, + ) } } } @Composable -private fun EmptySearchHints() { - Column( - modifier = Modifier.fillMaxWidth().padding(top = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, +private fun SearchEmptyState( + historyItems: List, + savedSearches: List, + onLoadQuery: (com.vitorpamplona.amethyst.commons.search.SearchQuery) -> Unit, + onDeleteSaved: (String) -> Unit, + onClearHistory: () -> Unit, +) { + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { - Text( - "Search for users, notes, or content", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(Modifier.height(8.dp)) - Text( - "Use operators to refine your search:", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - ) - Spacer(Modifier.height(16.dp)) - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - SearchHint("from:npub1...", "Filter by author") - SearchHint("kind:article", "Long-form content") - SearchHint("since:2025-01", "After January 2025") - SearchHint("#bitcoin", "Hashtag search") - SearchHint("\"exact phrase\"", "Exact match") - SearchHint("bitcoin OR nostr", "Either term") - SearchHint("-spam", "Exclude term") - SearchHint("lang:en", "Language filter") + // Saved searches + if (savedSearches.isNotEmpty()) { + item { + Text( + "Saved Searches", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 4.dp), + ) + } + items(savedSearches, key = { "saved-${it.id}" }) { saved -> + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { onLoadQuery(saved.query) } + .padding(vertical = 6.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.Default.Star, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Column(modifier = Modifier.weight(1f)) { + Text( + saved.label, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + QuerySerializer.serialize(saved.query), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontFamily = FontFamily.Monospace, + ) + } + IconButton(onClick = { onDeleteSaved(saved.id) }) { + Icon( + Icons.Default.Delete, + contentDescription = "Delete", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + item { HorizontalDivider(Modifier.padding(vertical = 8.dp)) } } + + // Recent history + if (historyItems.isNotEmpty()) { + item { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + "Recent", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + TextButton(onClick = onClearHistory) { + Text("Clear", style = MaterialTheme.typography.labelSmall) + } + } + } + items(historyItems.take(10), key = { "history-${QuerySerializer.serialize(it)}" }) { query -> + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { onLoadQuery(query) } + .padding(vertical = 6.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + Icons.Default.History, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + QuerySerializer.serialize(query), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + fontFamily = FontFamily.Monospace, + modifier = Modifier.weight(1f), + ) + } + } + item { HorizontalDivider(Modifier.padding(vertical = 8.dp)) } + } + + // Operator hints + item { + Column( + modifier = Modifier.fillMaxWidth().padding(top = if (historyItems.isEmpty() && savedSearches.isEmpty()) 32.dp else 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + "Search operators", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(Modifier.height(8.dp)) + } + } + item { SearchHint("from:npub1...", "Filter by author") } + item { SearchHint("kind:article", "Long-form content") } + item { SearchHint("since:2025-01", "After January 2025") } + item { SearchHint("#bitcoin", "Hashtag search") } + item { SearchHint("\"exact phrase\"", "Exact match") } + item { SearchHint("bitcoin OR nostr", "Either term") } + item { SearchHint("-spam", "Exclude term") } + item { SearchHint("lang:en", "Language filter") } } } From e8e2650fa9ef0a45750b452bbec838c083f646df Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 11 Mar 2026 07:21:17 +0200 Subject: [PATCH 007/172] refactor(search): extract shared code to commons, remove dead code - Extract DateUtils (isLeapYear, dateToUnix, timestampToDate) to deduplicate identical implementations in QueryParser and QuerySerializer - Move SavedSearch data class from desktopApp to commons/search - Replace local formatTimestamp with existing toTimeAgo from commons/util - Remove dead code: resolveAuthorName, _authorSuggestions (never called) - Remove dead code: createOrFilters (never wired into SearchScreen) - Remove unused ICacheProvider dependency from AdvancedSearchBarState Co-Authored-By: Claude Opus 4.6 --- .../commons/search/AdvancedSearchBarState.kt | 17 ----- .../amethyst/commons/search/DateUtils.kt | 71 +++++++++++++++++++ .../amethyst/commons/search/QueryParser.kt | 25 +------ .../commons/search/QuerySerializer.kt | 27 +------ .../amethyst/commons/search/SavedSearch.kt | 28 ++++++++ .../amethyst/desktop/SearchHistoryStore.kt | 8 +-- .../subscriptions/SearchFilterFactory.kt | 13 ---- .../amethyst/desktop/ui/SearchScreen.kt | 4 +- .../desktop/ui/search/SearchResultsList.kt | 32 +-------- 9 files changed, 106 insertions(+), 119 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DateUtils.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SavedSearch.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt index ba054a8011..e46bcef974 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.commons.search import com.vitorpamplona.amethyst.commons.model.User -import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.nip01Core.core.Event import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -44,7 +43,6 @@ enum class ChangeSource { @OptIn(FlowPreview::class) class AdvancedSearchBarState( - private val cache: ICacheProvider, private val scope: CoroutineScope, private val debounceMs: Long = 300L, ) { @@ -82,10 +80,6 @@ class AdvancedSearchBarState( private val _isSearching = MutableStateFlow(false) val isSearching: StateFlow = _isSearching.asStateFlow() - // Author name suggestions for autocomplete - private val _authorSuggestions = MutableStateFlow>(persistentListOf()) - val authorSuggestions: StateFlow> = _authorSuggestions.asStateFlow() - // Expanded panel state private val _panelExpanded = MutableStateFlow(false) val panelExpanded: StateFlow = _panelExpanded.asStateFlow() @@ -214,15 +208,4 @@ class AdvancedSearchBarState( _noteResults.value = (current + newEvents).sortedByDescending { it.createdAt }.toImmutableList() } } - - // Name resolution for autocomplete - @Suppress("UNCHECKED_CAST") - fun resolveAuthorName(name: String) { - if (name.length < 2) { - _authorSuggestions.value = persistentListOf() - return - } - val results = cache.findUsersStartingWith(name, 5) as List - _authorSuggestions.value = results.toImmutableList() - } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DateUtils.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DateUtils.kt new file mode 100644 index 0000000000..e7b1d053bf --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/DateUtils.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +object DateUtils { + fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + + fun dateToUnix( + year: Int, + month: Int, + day: Int, + ): Long { + var totalDays = 0L + + for (y in 1970 until year) { + totalDays += if (isLeapYear(y)) 366 else 365 + } + + val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + if (isLeapYear(year)) daysInMonth[2] = 29 + for (m in 1 until month) { + totalDays += daysInMonth[m] + } + + totalDays += (day - 1) + + return totalDays * 86400L + } + + fun timestampToDate(timestamp: Long): String { + var remaining = timestamp + var year = 1970 + while (true) { + val daysInYear = if (isLeapYear(year)) 366L else 365L + val secondsInYear = daysInYear * 86400L + if (remaining < secondsInYear) break + remaining -= secondsInYear + year++ + } + + val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) + if (isLeapYear(year)) daysInMonth[2] = 29 + + var dayOfYear = (remaining / 86400).toInt() + 1 + var month = 1 + while (month <= 12 && dayOfYear > daysInMonth[month]) { + dayOfYear -= daysInMonth[month] + month++ + } + + return "$year-${month.toString().padStart(2, '0')}-${dayOfYear.toString().padStart(2, '0')}" + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt index be1a93e34b..073e9cc41f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParser.kt @@ -319,28 +319,5 @@ object QueryParser { year: Int, month: Int, day: Int, - ): Long { - // Simple UTC date to unix timestamp calculation - // Days from epoch (1970-01-01) to the given date - var totalDays = 0L - - // Add days for full years - for (y in 1970 until year) { - totalDays += if (isLeapYear(y)) 366 else 365 - } - - // Add days for full months in target year - val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - if (isLeapYear(year)) daysInMonth[2] = 29 - for (m in 1 until month) { - totalDays += daysInMonth[m] - } - - // Add remaining days - totalDays += (day - 1) - - return totalDays * 86400L - } - - private fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + ): Long = DateUtils.dateToUnix(year, month, day) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt index dd7b0612aa..3cd27a0178 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/QuerySerializer.kt @@ -84,30 +84,5 @@ object QuerySerializer { return parts.joinToString(" ") } - fun timestampToDate(timestamp: Long): String { - // Convert unix timestamp to YYYY-MM-DD - var remaining = timestamp - var year = 1970 - while (true) { - val daysInYear = if (isLeapYear(year)) 366L else 365L - val secondsInYear = daysInYear * 86400L - if (remaining < secondsInYear) break - remaining -= secondsInYear - year++ - } - - val daysInMonth = intArrayOf(0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) - if (isLeapYear(year)) daysInMonth[2] = 29 - - var dayOfYear = (remaining / 86400).toInt() + 1 - var month = 1 - while (month <= 12 && dayOfYear > daysInMonth[month]) { - dayOfYear -= daysInMonth[month] - month++ - } - - return "$year-${month.toString().padStart(2, '0')}-${dayOfYear.toString().padStart(2, '0')}" - } - - private fun isLeapYear(year: Int): Boolean = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) + fun timestampToDate(timestamp: Long): String = DateUtils.timestampToDate(timestamp) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SavedSearch.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SavedSearch.kt new file mode 100644 index 0000000000..20302fba73 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SavedSearch.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +data class SavedSearch( + val id: String, + val label: String, + val query: SearchQuery, + val createdAt: Long, +) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt index 8271c1e67f..9c3e3b01ad 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/SearchHistoryStore.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.desktop import com.vitorpamplona.amethyst.commons.search.QueryParser import com.vitorpamplona.amethyst.commons.search.QuerySerializer +import com.vitorpamplona.amethyst.commons.search.SavedSearch import com.vitorpamplona.amethyst.commons.search.SearchQuery import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -134,10 +135,3 @@ object SearchHistoryStore { prefs.put(KEY_SAVED, raw) } } - -data class SavedSearch( - val id: String, - val label: String, - val query: SearchQuery, - val createdAt: Long, -) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt index 7aa491f714..6a61b000d7 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SearchFilterFactory.kt @@ -128,19 +128,6 @@ object SearchFilterFactory { } } - fun createOrFilters( - query: SearchQuery, - limit: Int = 100, - ): List> { - if (query.orTerms.isEmpty()) return listOf(createFilters(query, limit)) - - // Each OR term gets its own set of filters - return query.orTerms.take(3).map { term -> - val termQuery = query.copy(text = term, orTerms = kotlinx.collections.immutable.persistentListOf()) - createFilters(termQuery, limit) - } - } - private fun buildSearchString(query: SearchQuery): String? { val parts = mutableListOf() diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index d7fd9a6439..885b48843c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -111,7 +111,7 @@ fun SearchScreen( modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() - val state = remember { AdvancedSearchBarState(localCache, scope) } + val state = remember { AdvancedSearchBarState(scope) } val focusRequester = remember { FocusRequester() } // Pre-fill initial query @@ -408,7 +408,7 @@ fun SearchScreen( @Composable private fun SearchEmptyState( historyItems: List, - savedSearches: List, + savedSearches: List, onLoadQuery: (com.vitorpamplona.amethyst.commons.search.SearchQuery) -> Unit, onDeleteSaved: (String) -> Unit, onClearHistory: () -> Unit, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt index e26d6a462b..93a62e39e5 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -62,6 +62,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState import com.vitorpamplona.amethyst.commons.search.KindRegistry import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard +import com.vitorpamplona.amethyst.commons.util.toTimeAgo import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent @@ -255,7 +256,7 @@ private fun NotePreviewCard( Spacer(Modifier.weight(1f)) // Timestamp Text( - formatTimestamp(event.createdAt), + event.createdAt.toTimeAgo(withDot = false), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, ) @@ -296,32 +297,3 @@ private fun ExpandableSection( } } } - -private fun formatTimestamp(ts: Long): String { - val now = System.currentTimeMillis() / 1000 - val diff = now - ts - return when { - diff < 60 -> { - "just now" - } - - diff < 3600 -> { - "${diff / 60}m ago" - } - - diff < 86400 -> { - "${diff / 3600}h ago" - } - - diff < 604800 -> { - "${diff / 86400}d ago" - } - - else -> { - // Simple date format - val days = ts / 86400 - val year = 1970 + (days / 365).toInt() - "$year" - } - } -} From a702dcc0ee12525d4f9e5ba7da41d46063ec8611 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 11 Mar 2026 07:28:04 +0200 Subject: [PATCH 008/172] =?UTF-8?q?fix(search):=20cleanup=20review=20findi?= =?UTF-8?q?ngs=20=E2=80=94=20dead=20code,=20FQN=20refs,=20naming?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused `key` param from ExpandableSection - Replace FQN references with imports in AdvancedSearchPanel and SearchScreen - Use MetadataEvent.KIND instead of magic `0` - Remove dead `relayStatuses` collection (only `connectedRelays` used) - Remove CachedUserResult sealed variant (never constructed) - Rename SearchHint `identifier` param to `example` Co-Authored-By: Claude Opus 4.6 --- .../amethyst/commons/search/SearchResult.kt | 9 -------- .../amethyst/desktop/ui/SearchScreen.kt | 22 ++++++++----------- .../desktop/ui/search/AdvancedSearchPanel.kt | 14 +++++------- .../desktop/ui/search/SearchResultsList.kt | 4 ---- 4 files changed, 15 insertions(+), 34 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResult.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResult.kt index dc77eba392..21cd3a0a24 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResult.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResult.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.search -import com.vitorpamplona.amethyst.commons.model.User - /** * Represents a parsed search result from Bech32/hex input. * Shared between Android and Desktop for consistent search behavior. @@ -35,13 +33,6 @@ sealed class SearchResult { val displayId: String, ) : SearchResult() - /** - * User from local cache with full metadata. - */ - data class CachedUserResult( - val user: User, - ) : SearchResult() - /** * Note lookup from note1 or nevent. */ diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index 885b48843c..74323e8d45 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -81,6 +81,8 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState import com.vitorpamplona.amethyst.commons.search.QuerySerializer +import com.vitorpamplona.amethyst.commons.search.SavedSearch +import com.vitorpamplona.amethyst.commons.search.SearchQuery import com.vitorpamplona.amethyst.commons.search.SearchResult import com.vitorpamplona.amethyst.commons.search.SearchResultFilter import com.vitorpamplona.amethyst.commons.search.parseSearchInput @@ -121,7 +123,6 @@ fun SearchScreen( } } - val relayStatuses by relayManager.relayStatuses.collectAsState() val connectedRelays by relayManager.connectedRelays.collectAsState() val displayText by state.displayText.collectAsState() val query by state.query.collectAsState() @@ -149,8 +150,7 @@ fun SearchScreen( relays = connectedRelays, searchQuery = debouncedQuery.text.ifBlank { - com.vitorpamplona.amethyst.commons.search.QuerySerializer - .serialize(debouncedQuery) + QuerySerializer.serialize(debouncedQuery) }, limit = 20, onEvent = { event, _, _, _ -> @@ -185,7 +185,7 @@ fun SearchScreen( relays = connectedRelays, onEvent = { event, _, _, _ -> // Skip metadata events (handled by people search) - if (event.kind == 0) return@SubscriptionConfig + if (event.kind == MetadataEvent.KIND) return@SubscriptionConfig val filtered = SearchResultFilter.filter(listOf(event), debouncedQuery) if (filtered.isNotEmpty()) { state.addNoteResults(filtered) @@ -407,9 +407,9 @@ fun SearchScreen( @Composable private fun SearchEmptyState( - historyItems: List, - savedSearches: List, - onLoadQuery: (com.vitorpamplona.amethyst.commons.search.SearchQuery) -> Unit, + historyItems: List, + savedSearches: List, + onLoadQuery: (SearchQuery) -> Unit, onDeleteSaved: (String) -> Unit, onClearHistory: () -> Unit, ) { @@ -542,7 +542,7 @@ private fun SearchEmptyState( @Composable private fun SearchHint( - identifier: String, + example: String, description: String, ) { Row( @@ -550,7 +550,7 @@ private fun SearchHint( horizontalArrangement = Arrangement.SpaceBetween, ) { Text( - identifier, + example, style = MaterialTheme.typography.bodySmall, fontFamily = FontFamily.Monospace, color = MaterialTheme.colorScheme.primary, @@ -578,7 +578,6 @@ private fun SearchResultCard( .clickable { when (result) { is SearchResult.UserResult -> onNavigateToProfile(result.pubKeyHex) - is SearchResult.CachedUserResult -> onNavigateToProfile(result.user.pubkeyHex) is SearchResult.NoteResult -> onNavigateToThread(result.noteIdHex) is SearchResult.AddressResult -> onNavigateToThread("${result.kind}:${result.pubKeyHex}:${result.dTag}") is SearchResult.HashtagResult -> onNavigateToHashtag(result.hashtag) @@ -598,7 +597,6 @@ private fun SearchResultCard( imageVector = when (result) { is SearchResult.UserResult -> Icons.Default.Person - is SearchResult.CachedUserResult -> Icons.Default.Person is SearchResult.NoteResult -> Icons.Default.Description is SearchResult.AddressResult -> Icons.Default.Description is SearchResult.HashtagResult -> Icons.Default.Tag @@ -612,7 +610,6 @@ private fun SearchResultCard( Text( when (result) { is SearchResult.UserResult -> "User Profile" - is SearchResult.CachedUserResult -> result.user.toBestDisplayName() is SearchResult.NoteResult -> "Note" is SearchResult.AddressResult -> "Event (kind ${result.kind})" is SearchResult.HashtagResult -> "#${result.hashtag}" @@ -623,7 +620,6 @@ private fun SearchResultCard( Text( when (result) { is SearchResult.UserResult -> result.displayId - is SearchResult.CachedUserResult -> result.user.pubkeyDisplayHex() is SearchResult.NoteResult -> result.displayId is SearchResult.AddressResult -> result.displayId is SearchResult.HashtagResult -> "Search posts with this hashtag" diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt index 33de9faec0..ec1eeae199 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt @@ -56,7 +56,9 @@ import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.search.DateUtils import com.vitorpamplona.amethyst.commons.search.KindRegistry +import com.vitorpamplona.amethyst.commons.search.QueryParser import com.vitorpamplona.amethyst.commons.search.SearchQuery @OptIn(ExperimentalLayoutApi::class) @@ -243,16 +245,14 @@ private fun DateRangeFields( var sinceText by remember(since) { mutableStateOf( since?.let { - com.vitorpamplona.amethyst.commons.search.QuerySerializer - .timestampToDate(it) + DateUtils.timestampToDate(it) } ?: "", ) } var untilText by remember(until) { mutableStateOf( until?.let { - com.vitorpamplona.amethyst.commons.search.QuerySerializer - .timestampToDate(it) + DateUtils.timestampToDate(it) } ?: "", ) } @@ -272,8 +272,7 @@ private fun DateRangeFields( onValueChange = { sinceText = it val ts = - com.vitorpamplona.amethyst.commons.search.QueryParser - .parseDateToTimestamp(it) + QueryParser.parseDateToTimestamp(it) onChanged(ts, until) }, modifier = Modifier.fillMaxWidth(), @@ -292,8 +291,7 @@ private fun DateRangeFields( onValueChange = { untilText = it val ts = - com.vitorpamplona.amethyst.commons.search.QueryParser - .parseDateToTimestamp(it) + QueryParser.parseDateToTimestamp(it) onChanged(since, ts) }, modifier = Modifier.fillMaxWidth(), diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt index 93a62e39e5..f51d186efa 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -118,7 +118,6 @@ fun SearchResultsList( item(key = "people-expand") { ExpandableSection( remaining = people.drop(5), - key = { "person-more-${it.pubkeyHex}" }, ) { user -> UserSearchCard( user = user, @@ -145,7 +144,6 @@ fun SearchResultsList( item(key = "notes-expand") { ExpandableSection( remaining = textNotes.drop(5), - key = { "note-more-${it.id}" }, ) { event -> NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) } @@ -168,7 +166,6 @@ fun SearchResultsList( item(key = "articles-expand") { ExpandableSection( remaining = articles.drop(5), - key = { "article-more-${it.id}" }, ) { event -> NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) } @@ -277,7 +274,6 @@ private fun NotePreviewCard( @Composable private fun ExpandableSection( remaining: List, - key: (T) -> String, content: @Composable (T) -> Unit, ) { var expanded by remember { mutableStateOf(false) } From 979d07679cc0b7c0f87249e5b4c80d70499ba067 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 11 Mar 2026 09:29:27 +0200 Subject: [PATCH 009/172] fix(search): loading indicator, result delivery, and history race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace boolean isSearching with counter-based activeSubscriptionCount so loading state stays true until both subs (people + notes) complete - Add indeterminate LinearProgressIndicator overlaid on search bar bottom border with matching 12dp rounding, replacing "Searching relays..." text - Send search REQs to all configured relays (relayStatuses) instead of only connected ones — fixes NIP-50 relays never receiving search queries - Move clearResults/startSearching out of remember() into LaunchedEffect to avoid side effects during composition - Fix history saving partial queries by replacing LaunchedEffect with snapshotFlow to properly observe isSearching transitions Co-Authored-By: Claude Opus 4.6 --- .../commons/search/AdvancedSearchBarState.kt | 15 +- .../amethyst/desktop/ui/SearchScreen.kt | 131 ++++++++++-------- .../desktop/ui/search/SearchResultsList.kt | 11 -- 3 files changed, 86 insertions(+), 71 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt index e46bcef974..6a6e23fa1d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt @@ -33,7 +33,9 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update enum class ChangeSource { TEXT, @@ -77,8 +79,11 @@ class AdvancedSearchBarState( private val _noteResults = MutableStateFlow>(persistentListOf()) val noteResults: StateFlow> = _noteResults.asStateFlow() - private val _isSearching = MutableStateFlow(false) - val isSearching: StateFlow = _isSearching.asStateFlow() + private val activeSubscriptionCount = MutableStateFlow(0) + val isSearching: StateFlow = + activeSubscriptionCount + .map { it > 0 } + .stateIn(scope, SharingStarted.Eagerly, false) // Expanded panel state private val _panelExpanded = MutableStateFlow(false) @@ -176,16 +181,16 @@ class AdvancedSearchBarState( _query.value = SearchQuery.EMPTY _peopleResults.value = persistentListOf() _noteResults.value = persistentListOf() - _isSearching.value = false + activeSubscriptionCount.value = 0 } // Results management (called from subscription callbacks) fun startSearching() { - _isSearching.value = true + activeSubscriptionCount.update { it + 1 } } fun stopSearching() { - _isSearching.value = false + activeSubscriptionCount.update { maxOf(0, it - 1) } } fun clearResults() { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index 74323e8d45..2d87dad251 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -27,6 +27,7 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -55,6 +56,7 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -65,8 +67,10 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.key.Key @@ -124,6 +128,8 @@ fun SearchScreen( } val connectedRelays by relayManager.connectedRelays.collectAsState() + val relayStatuses by relayManager.relayStatuses.collectAsState() + val allRelayUrls = remember(relayStatuses) { relayStatuses.keys } val displayText by state.displayText.collectAsState() val query by state.query.collectAsState() val debouncedQuery by state.debouncedQuery.collectAsState() @@ -135,19 +141,24 @@ fun SearchScreen( // Bech32 parsing (immediate, no debounce) val bech32Results = remember(displayText) { parseSearchInput(displayText) } - // NIP-50 people search subscription + // Clear results and start loading when query changes + LaunchedEffect(debouncedQuery) { + if (!debouncedQuery.isEmpty && bech32Results.isEmpty()) { + state.clearResults() + state.startSearching() + state.startSearching() + } + } + + // NIP-50 people search subscription (use allRelayUrls — openReqSubscription will connect) rememberSubscription(connectedRelays, debouncedQuery, relayManager = relayManager) { - if (connectedRelays.isEmpty() || debouncedQuery.isEmpty) { + if (allRelayUrls.isEmpty() || debouncedQuery.isEmpty) { return@rememberSubscription null } - // Skip if bech32 detected if (bech32Results.isNotEmpty()) return@rememberSubscription null - state.startSearching() - state.clearResults() - createSearchPeopleSubscription( - relays = connectedRelays, + relays = allRelayUrls, searchQuery = debouncedQuery.text.ifBlank { QuerySerializer.serialize(debouncedQuery) @@ -169,9 +180,9 @@ fun SearchScreen( ) } - // NIP-50 advanced note search subscription (kinds beyond people) + // NIP-50 advanced note search subscription (use allRelayUrls) rememberSubscription(connectedRelays, debouncedQuery, relayManager = relayManager) { - if (connectedRelays.isEmpty() || debouncedQuery.isEmpty) { + if (allRelayUrls.isEmpty() || debouncedQuery.isEmpty) { return@rememberSubscription null } if (bech32Results.isNotEmpty()) return@rememberSubscription null @@ -182,9 +193,8 @@ fun SearchScreen( SubscriptionConfig( subId = generateSubId("adv-search"), filters = filters, - relays = connectedRelays, + relays = allRelayUrls, onEvent = { event, _, _, _ -> - // Skip metadata events (handled by people search) if (event.kind == MetadataEvent.KIND) return@SubscriptionConfig val filtered = SearchResultFilter.filter(listOf(event), debouncedQuery) if (filtered.isNotEmpty()) { @@ -218,11 +228,14 @@ fun SearchScreen( } } - // Save to history when search completes - LaunchedEffect(isSearching, debouncedQuery) { - if (!isSearching && !debouncedQuery.isEmpty) { - SearchHistoryStore.addToHistory(debouncedQuery) - } + // Save to history when search completes (snapshotFlow avoids LaunchedEffect race) + LaunchedEffect(Unit) { + snapshotFlow { isSearching to debouncedQuery } + .collect { (searching, query) -> + if (!searching && !query.isEmpty) { + SearchHistoryStore.addToHistory(query) + } + } } // History state @@ -281,39 +294,53 @@ fun SearchScreen( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - OutlinedTextField( - value = - TextFieldValue( - text = displayText, - selection = TextRange(displayText.length), - ), - onValueChange = { state.updateFromText(it.text) }, - modifier = - Modifier - .weight(1f) - .focusRequester(focusRequester), - placeholder = { Text("Search notes, people, tags... or use operators") }, - leadingIcon = { - Icon( - Icons.Default.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - trailingIcon = { - if (displayText.isNotEmpty()) { - IconButton(onClick = { state.clearSearch() }) { - Icon( - Icons.Default.Clear, - contentDescription = "Clear", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Box(modifier = Modifier.weight(1f)) { + OutlinedTextField( + value = + TextFieldValue( + text = displayText, + selection = TextRange(displayText.length), + ), + onValueChange = { state.updateFromText(it.text) }, + modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), + placeholder = { Text("Search notes, people, tags... or use operators") }, + leadingIcon = { + Icon( + Icons.Default.Search, + contentDescription = "Search", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + trailingIcon = { + if (displayText.isNotEmpty()) { + IconButton(onClick = { state.clearSearch() }) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - } - }, - singleLine = true, - shape = RoundedCornerShape(12.dp), - ) + }, + singleLine = true, + shape = RoundedCornerShape(12.dp), + ) + androidx.compose.animation.AnimatedVisibility( + visible = isSearching, + modifier = + Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(horizontal = 1.dp) + .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)), + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } + } IconButton(onClick = { state.togglePanel() }) { Icon( Icons.Default.Tune, @@ -386,13 +413,7 @@ fun SearchScreen( color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium, ) - } else if (isSearching) { - Text( - "Searching relays...", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - ) - } else { + } else if (!isSearching) { // Empty state: show history + saved searches + operator hints SearchEmptyState( historyItems = historyItems, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt index f51d186efa..b5bce1111f 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -76,20 +76,9 @@ fun SearchResultsList( ) { val people by state.peopleResults.collectAsState() val notes by state.noteResults.collectAsState() - val isSearching by state.isSearching.collectAsState() val hasResults = people.isNotEmpty() || notes.isNotEmpty() - if (!hasResults && isSearching) { - Text( - "Searching relays...", - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium, - modifier = modifier.padding(vertical = 8.dp), - ) - return - } - if (!hasResults) return // Group notes by kind From 3c6c368582a45b8e722eac8667fe7113ef9c2cef Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 11 Mar 2026 11:36:39 +0200 Subject: [PATCH 010/172] refactor(search): extract thread-safe EventDeduplicator, unify dedup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace raw MutableSet with synchronized EventDeduplicator class. Remove dual dedup in addNoteResults — trackRelayEvent now gates all event processing. SearchScreen callbacks skip dupes early via return value. Co-Authored-By: Claude Opus 4.6 --- .../commons/search/AdvancedSearchBarState.kt | 91 +++++++-- .../commons/search/EventDeduplicator.kt | 34 ++++ .../amethyst/desktop/network/RelayStatus.kt | 1 + .../desktop/subscriptions/FeedSubscription.kt | 2 + .../subscriptions/SubscriptionUtils.kt | 9 + .../amethyst/desktop/ui/SearchScreen.kt | 152 ++++++++------- .../desktop/ui/search/SearchSyncBanner.kt | 179 ++++++++++++++++++ 7 files changed, 390 insertions(+), 78 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/EventDeduplicator.kt create mode 100644 desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchSyncBanner.kt diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt index 6a6e23fa1d..5a375ebc96 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt @@ -20,8 +20,12 @@ */ package com.vitorpamplona.amethyst.commons.search +import com.vitorpamplona.amethyst.commons.chess.RelaySyncState +import com.vitorpamplona.amethyst.commons.chess.RelaySyncStatus import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.displayUrl import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -79,16 +83,22 @@ class AdvancedSearchBarState( private val _noteResults = MutableStateFlow>(persistentListOf()) val noteResults: StateFlow> = _noteResults.asStateFlow() - private val activeSubscriptionCount = MutableStateFlow(0) + private val activeSubIds = MutableStateFlow>(emptySet()) val isSearching: StateFlow = - activeSubscriptionCount - .map { it > 0 } + activeSubIds + .map { it.isNotEmpty() } .stateIn(scope, SharingStarted.Eagerly, false) + private val eventDeduplicator = EventDeduplicator() + // Expanded panel state private val _panelExpanded = MutableStateFlow(false) val panelExpanded: StateFlow = _panelExpanded.asStateFlow() + // Per-relay sync status + private val _relayStates = MutableStateFlow>(persistentListOf()) + val relayStates: StateFlow> = _relayStates.asStateFlow() + // Text bar input fun updateFromText(rawText: String) { _changeSource = ChangeSource.TEXT @@ -171,6 +181,49 @@ class AdvancedSearchBarState( _query.value = _query.value.copy(language = lang) } + fun initRelayStates(relays: Set) { + _relayStates.value = + relays + .map { + RelaySyncState( + url = it.url, + displayName = it.displayUrl(), + status = RelaySyncStatus.WAITING, + ) + }.toImmutableList() + } + + fun updateRelayState( + relayUrl: String, + status: RelaySyncStatus, + eventsDelta: Int = 0, + ) { + _relayStates.update { states -> + states + .map { + if (it.url == relayUrl) { + it.copy(status = status, eventsReceived = it.eventsReceived + eventsDelta) + } else { + it + } + }.toImmutableList() + } + } + + fun timeoutWaitingRelays() { + _relayStates.update { states -> + states + .map { + if (it.status == RelaySyncStatus.WAITING || it.status == RelaySyncStatus.CONNECTING) { + it.copy(status = RelaySyncStatus.FAILED) + } else { + it + } + }.toImmutableList() + } + activeSubIds.value = emptySet() + } + fun togglePanel() { _panelExpanded.value = !_panelExpanded.value } @@ -181,21 +234,35 @@ class AdvancedSearchBarState( _query.value = SearchQuery.EMPTY _peopleResults.value = persistentListOf() _noteResults.value = persistentListOf() - activeSubscriptionCount.value = 0 + _relayStates.value = persistentListOf() + activeSubIds.value = emptySet() + eventDeduplicator.clear() } // Results management (called from subscription callbacks) - fun startSearching() { - activeSubscriptionCount.update { it + 1 } + fun startSearching(subId: String) { + activeSubIds.update { it + subId } } - fun stopSearching() { - activeSubscriptionCount.update { maxOf(0, it - 1) } + fun stopSearching(subId: String) { + activeSubIds.update { it - subId } + } + + fun trackRelayEvent( + relayUrl: String, + eventId: String, + ): Boolean { + val isNew = eventDeduplicator.tryAdd(eventId) + if (isNew) { + updateRelayState(relayUrl, RelaySyncStatus.RECEIVING, eventsDelta = 1) + } + return isNew } fun clearResults() { _peopleResults.value = persistentListOf() _noteResults.value = persistentListOf() + eventDeduplicator.clear() } fun addPeopleResult(user: User) { @@ -206,11 +273,9 @@ class AdvancedSearchBarState( } fun addNoteResults(events: List) { - val current = _noteResults.value - val existingIds = current.map { it.id }.toSet() - val newEvents = events.filter { it.id !in existingIds } - if (newEvents.isNotEmpty()) { - _noteResults.value = (current + newEvents).sortedByDescending { it.createdAt }.toImmutableList() + if (events.isNotEmpty()) { + val current = _noteResults.value + _noteResults.value = (current + events).sortedByDescending { it.createdAt }.toImmutableList() } } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/EventDeduplicator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/EventDeduplicator.kt new file mode 100644 index 0000000000..8a843955ae --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/EventDeduplicator.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +class EventDeduplicator { + private val lock = Any() + private val seenIds = mutableSetOf() + + fun tryAdd(id: String): Boolean = synchronized(lock) { seenIds.add(id) } + + fun contains(id: String): Boolean = synchronized(lock) { id in seenIds } + + fun clear() = synchronized(lock) { seenIds.clear() } + + val size: Int get() = synchronized(lock) { seenIds.size } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt index 24c092b909..32f69dac9a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt @@ -45,5 +45,6 @@ object DefaultRelays { "wss://nos.lol", "wss://relay.snort.social", "wss://nostr.wine", + "wss://relay.noswhere.com", ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt index d4d3bac207..a7d4a633a3 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt @@ -145,6 +145,7 @@ fun createSearchPeopleSubscription( limit: Int = 50, onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, + onClosed: (NormalizedRelayUrl, String, List?) -> Unit = { _, _, _ -> }, ): SubscriptionConfig? { if (searchQuery.isBlank()) return null @@ -154,6 +155,7 @@ fun createSearchPeopleSubscription( relays = relays, onEvent = onEvent, onEose = onEose, + onClosed = onClosed, ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt index 3ecac1b056..ab02383049 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt @@ -46,6 +46,7 @@ data class SubscriptionConfig( val relays: Set, val onEvent: (Event, Boolean, NormalizedRelayUrl, List?) -> Unit, val onEose: (NormalizedRelayUrl, List?) -> Unit = { _, _ -> }, + val onClosed: (NormalizedRelayUrl, String, List?) -> Unit = { _, _, _ -> }, ) /** @@ -95,6 +96,14 @@ fun rememberSubscription( ) { cfg.onEose(relay, forFilters) } + + override fun onClosed( + message: String, + relay: NormalizedRelayUrl, + forFilters: List?, + ) { + cfg.onClosed(relay, message, forFilters) + } }, ) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index 2d87dad251..ba29094af4 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -70,7 +69,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.key.Key @@ -82,6 +80,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.chess.RelaySyncStatus import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState import com.vitorpamplona.amethyst.commons.search.QuerySerializer @@ -102,6 +101,7 @@ import com.vitorpamplona.amethyst.desktop.subscriptions.generateSubId import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.desktop.ui.search.AdvancedSearchPanel import com.vitorpamplona.amethyst.desktop.ui.search.SearchResultsList +import com.vitorpamplona.amethyst.desktop.ui.search.SearchSyncBanner import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull @@ -137,6 +137,7 @@ fun SearchScreen( val isSearching by state.isSearching.collectAsState() val peopleResults by state.peopleResults.collectAsState() val noteResults by state.noteResults.collectAsState() + val relayStates by state.relayStates.collectAsState() // Bech32 parsing (immediate, no debounce) val bech32Results = remember(displayText) { parseSearchInput(displayText) } @@ -145,8 +146,12 @@ fun SearchScreen( LaunchedEffect(debouncedQuery) { if (!debouncedQuery.isEmpty && bech32Results.isEmpty()) { state.clearResults() - state.startSearching() - state.startSearching() + state.initRelayStates(allRelayUrls) + state.startSearching("people-search") + state.startSearching("adv-search") + // Timeout relays that silently ignore NIP-50 (e.g. strfry) + kotlinx.coroutines.delay(10_000L) + state.timeoutWaitingRelays() } } @@ -164,18 +169,25 @@ fun SearchScreen( QuerySerializer.serialize(debouncedQuery) }, limit = 20, - onEvent = { event, _, _, _ -> - if (event is MetadataEvent) { - localCache.consumeMetadata(event) - @Suppress("UNCHECKED_CAST") - val user = localCache.getUserIfExists(event.pubKey) as? User - if (user != null) { - state.addPeopleResult(user) + onEvent = { event, _, relay, _ -> + if (state.trackRelayEvent(relay.url, event.id)) { + if (event is MetadataEvent) { + localCache.consumeMetadata(event) + @Suppress("UNCHECKED_CAST") + val user = localCache.getUserIfExists(event.pubKey) as? User + if (user != null) { + state.addPeopleResult(user) + } } } }, - onEose = { _, _ -> - state.stopSearching() + onEose = { relay, _ -> + state.updateRelayState(relay.url, RelaySyncStatus.EOSE_RECEIVED) + state.stopSearching("people-search") + }, + onClosed = { relay, _, _ -> + state.updateRelayState(relay.url, RelaySyncStatus.FAILED) + state.stopSearching("people-search") }, ) } @@ -194,15 +206,22 @@ fun SearchScreen( subId = generateSubId("adv-search"), filters = filters, relays = allRelayUrls, - onEvent = { event, _, _, _ -> + onEvent = { event, _, relay, _ -> if (event.kind == MetadataEvent.KIND) return@SubscriptionConfig - val filtered = SearchResultFilter.filter(listOf(event), debouncedQuery) - if (filtered.isNotEmpty()) { - state.addNoteResults(filtered) + if (state.trackRelayEvent(relay.url, event.id)) { + val filtered = SearchResultFilter.filter(listOf(event), debouncedQuery) + if (filtered.isNotEmpty()) { + state.addNoteResults(filtered) + } } }, - onEose = { _, _ -> - state.stopSearching() + onEose = { relay, _ -> + state.updateRelayState(relay.url, RelaySyncStatus.EOSE_RECEIVED) + state.stopSearching("adv-search") + }, + onClosed = { relay, _, _ -> + state.updateRelayState(relay.url, RelaySyncStatus.FAILED) + state.stopSearching("adv-search") }, ) } @@ -269,6 +288,26 @@ fun SearchScreen( } }, ) { + // Progress bar at very top + AnimatedVisibility( + visible = isSearching, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + ) { + LinearProgressIndicator( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } + + // Relay status banner + SearchSyncBanner( + relayStates = relayStates, + isSearching = isSearching, + ) + + // Title row Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -294,53 +333,36 @@ fun SearchScreen( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Box(modifier = Modifier.weight(1f)) { - OutlinedTextField( - value = - TextFieldValue( - text = displayText, - selection = TextRange(displayText.length), - ), - onValueChange = { state.updateFromText(it.text) }, - modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), - placeholder = { Text("Search notes, people, tags... or use operators") }, - leadingIcon = { - Icon( - Icons.Default.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - trailingIcon = { - if (displayText.isNotEmpty()) { - IconButton(onClick = { state.clearSearch() }) { - Icon( - Icons.Default.Clear, - contentDescription = "Clear", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - }, - singleLine = true, - shape = RoundedCornerShape(12.dp), - ) - androidx.compose.animation.AnimatedVisibility( - visible = isSearching, - modifier = - Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(horizontal = 1.dp) - .clip(RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp)), - ) { - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.surfaceVariant, + OutlinedTextField( + value = + TextFieldValue( + text = displayText, + selection = TextRange(displayText.length), + ), + onValueChange = { state.updateFromText(it.text) }, + modifier = Modifier.weight(1f).focusRequester(focusRequester), + placeholder = { Text("Search notes, people, tags... or use operators") }, + leadingIcon = { + Icon( + Icons.Default.Search, + contentDescription = "Search", + tint = MaterialTheme.colorScheme.onSurfaceVariant, ) - } - } + }, + trailingIcon = { + if (displayText.isNotEmpty()) { + IconButton(onClick = { state.clearSearch() }) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + }, + singleLine = true, + shape = RoundedCornerShape(12.dp), + ) IconButton(onClick = { state.togglePanel() }) { Icon( Icons.Default.Tune, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchSyncBanner.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchSyncBanner.kt new file mode 100644 index 0000000000..348dcaf0fe --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchSyncBanner.kt @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.desktop.ui.search + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.HourglassEmpty +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.chess.RelaySyncState +import com.vitorpamplona.amethyst.commons.chess.RelaySyncStatus +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun SearchSyncBanner( + relayStates: ImmutableList, + isSearching: Boolean, + modifier: Modifier = Modifier, +) { + val isVisible = isSearching || relayStates.isNotEmpty() + var isExpanded by remember { mutableStateOf(false) } + + AnimatedVisibility( + visible = isVisible, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + modifier = modifier, + ) { + Column { + // Collapsed summary row + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier + .fillMaxWidth() + .clickable { isExpanded = !isExpanded } + .padding(horizontal = 4.dp, vertical = 6.dp), + ) { + val responded = relayStates.count { it.status == RelaySyncStatus.EOSE_RECEIVED } + val total = relayStates.size + val totalEvents = relayStates.sumOf { it.eventsReceived } + + Text( + text = + if (total > 0) { + "$responded/$total relays responded \u00B7 $totalEvents events" + } else { + "Connecting to relays..." + }, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + + Icon( + imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = if (isExpanded) "Collapse" else "Expand", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp), + ) + } + + // Expanded per-relay details + AnimatedVisibility( + visible = isExpanded && relayStates.isNotEmpty(), + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut(), + ) { + Column { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + thickness = 0.5.dp, + ) + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 4.dp, vertical = 6.dp), + ) { + relayStates.forEach { relay -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Icon( + imageVector = relayStatusIcon(relay.status), + contentDescription = null, + tint = relayStatusColor(relay.status), + modifier = Modifier.size(14.dp), + ) + Text( + text = relay.displayName, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Text( + text = "${relay.eventsReceived} events", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + ) + } + } + } + } + } + } + } +} + +private fun relayStatusIcon(status: RelaySyncStatus): ImageVector = + when (status) { + RelaySyncStatus.CONNECTING -> Icons.Default.HourglassEmpty + RelaySyncStatus.WAITING -> Icons.Default.HourglassEmpty + RelaySyncStatus.RECEIVING -> Icons.Default.CloudDownload + RelaySyncStatus.EOSE_RECEIVED -> Icons.Default.CheckCircle + RelaySyncStatus.FAILED -> Icons.Default.Error + } + +@Composable +private fun relayStatusColor(status: RelaySyncStatus): Color = + when (status) { + RelaySyncStatus.CONNECTING -> MaterialTheme.colorScheme.secondary + RelaySyncStatus.WAITING -> MaterialTheme.colorScheme.secondary + RelaySyncStatus.RECEIVING -> MaterialTheme.colorScheme.primary + RelaySyncStatus.EOSE_RECEIVED -> MaterialTheme.colorScheme.primary + RelaySyncStatus.FAILED -> MaterialTheme.colorScheme.error + } From f0ffe3cbe39d106938555feef238e84f77d452fb Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Wed, 11 Mar 2026 11:36:46 +0200 Subject: [PATCH 011/172] fix(ui): remove duplicate back button from SinglePaneLayout Overlay screens (UserProfileScreen, ThreadScreen) already render their own back+title header. Remove the redundant SinglePaneHeader bar. Co-Authored-By: Claude Opus 4.6 --- .../desktop/ui/deck/SinglePaneLayout.kt | 56 ------------------- 1 file changed, 56 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt index dd82766997..e35e7c3fc3 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt @@ -20,20 +20,15 @@ */ package com.vitorpamplona.amethyst.desktop.ui.deck -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.Article import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.Email @@ -43,9 +38,7 @@ import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailItem @@ -57,7 +50,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextOverflow @@ -144,20 +136,6 @@ fun SinglePaneLayout( VerticalDivider() Column(modifier = Modifier.weight(1f).fillMaxHeight()) { - // Show header with back button when navigated into overlay - if (navStack.isNotEmpty()) { - SinglePaneHeader( - title = - when (currentOverlay) { - is DesktopScreen.UserProfile -> "Profile" - is DesktopScreen.Thread -> "Thread" - else -> currentColumnType.title() - }, - onBack = { navState.pop() }, - ) - HorizontalDivider() - } - Box( modifier = Modifier.fillMaxSize().padding(12.dp), ) { @@ -197,37 +175,3 @@ fun SinglePaneLayout( } } } - -@Composable -private fun SinglePaneHeader( - title: String, - onBack: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = - modifier - .fillMaxWidth() - .height(40.dp) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton(onClick = onBack, modifier = Modifier.size(28.dp)) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back", - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Spacer(Modifier.width(8.dp)) - Text( - text = title, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } -} From a94d5bfd070c4d98a80fbe8570c2631b684f3ecb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 11 Mar 2026 15:29:16 -0400 Subject: [PATCH 012/172] Fixes BUD 10 tests --- .../quartz/nipB7Blossom/BlossomUri.kt | 2 +- .../quartz/nipB7Blossom/BlossomUriTest.kt | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUri.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUri.kt index 202dc19b92..b2c06663c8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUri.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUri.kt @@ -55,7 +55,7 @@ data class BlossomUri( buildList { servers.forEach { add("xs=${percentEncodeQueryValue(it)}") } authors.forEach { add("as=$it") } - size?.let { add("sz=$it") } + this@BlossomUri.size?.let { add("sz=$it") } } if (params.isNotEmpty()) { append('?') diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUriTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUriTest.kt index 68be8cdb6f..72265e6f5a 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUriTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUriTest.kt @@ -131,4 +131,24 @@ class BlossomUriTest { assertNotNull(result) assertEquals(listOf(authorPubkey, author2), result.authors) } + + @Test + fun handlesMultipleAuthorsMultipleServers() { + val author1 = "781208004e09102d7da3b7345e64fd193cd1bc3fce8fdae6008d77f9cabcd036" + val author2 = "b53185b9f27962ebdf76b8a9b0a84cd8b27f9f3d4abd59f715788a3bf9e7f75e" + val server1 = "cdn.example.com" + val server2 = "media.nostr.build" + val fileHex = "a7b3c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1" + val fileExt = "png" + val size = 2547831L + val uri = "blossom:$fileHex.$fileExt?xs=$server1&xs=$server2&as=$author1&as=$author2&sz=$size" + val result = BlossomUri.parse(uri) + + assertNotNull(result) + assertEquals(fileHex, result.sha256) + assertEquals(fileExt, result.extension) + assertEquals(listOf(author1, author2), result.authors) + assertEquals(listOf(server1, server2), result.servers) + assertEquals(size, result.size) + } } From 3e16d317fc0e8cb601dc759ccc18899fbc15218c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Mar 2026 09:35:16 +0000 Subject: [PATCH 013/172] feat: add blockchain explorer settings page for OTS verification Adds a new "Bitcoin Explorer (OTS)" settings page in App Settings, modeled after the existing Namecoin settings page. Users can configure a custom Mempool-compatible blockchain explorer API URL for OpenTimestamps proof verification. When a custom URL is set it takes priority over the automatic Tor-aware selection (mempool.space when Tor is active, blockstream.info otherwise). Changes: - OtsSettings: immutable data class for explorer config - OtsSharedPreferences: DataStore-backed persistence (global/app-level) - TorAwareOkHttpOtsResolverBuilder: accepts customExplorerUrl lambda - OtsSettingsSection: UI section with active explorer display, URL input, known-explorers reference list, and reset button - OtsSettingsScreen: full screen wrapping the section - Routes.OtsSettings: new navigation route - AllSettingsScreen: new entry under App Settings - AppModules: wires otsPrefs and passes custom URL to resolver builder https://claude.ai/code/session_01GX7k36uepGxU3U1fBKYFrx --- .../com/vitorpamplona/amethyst/AppModules.kt | 7 + .../model/nip03Timestamp/OtsSettings.kt | 70 ++++ .../TorAwareOkHttpOtsResolverBuilder.kt | 14 +- .../model/preferences/OtsSharedPreferences.kt | 103 +++++ .../amethyst/ui/navigation/AppNavigation.kt | 2 + .../amethyst/ui/navigation/routes/Routes.kt | 2 + .../loggedIn/settings/AllSettingsScreen.kt | 8 + .../loggedIn/settings/OtsSettingsScreen.kt | 83 ++++ .../loggedIn/settings/OtsSettingsSection.kt | 369 ++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 1 + 10 files changed, 653 insertions(+), 6 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsSettings.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/OtsSharedPreferences.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsSection.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 5885c1e221..25db5b3f4c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier import com.vitorpamplona.amethyst.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder +import com.vitorpamplona.amethyst.model.preferences.OtsSharedPreferences import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever import com.vitorpamplona.amethyst.model.preferences.NamecoinSharedPreferences import com.vitorpamplona.amethyst.model.preferences.TorSharedPreferences @@ -111,6 +112,11 @@ class AppModules( NamecoinSharedPreferences(appContext, applicationIOScope) } + // OTS blockchain explorer preferences (global, like Tor settings) + val otsPrefs by lazy { + OtsSharedPreferences(appContext, applicationIOScope) + } + // App services that should be run as soon as there are subscribers to their flows val locationManager = LocationState(appContext, applicationIOScope) val connManager = ConnectivityManager(appContext, applicationIOScope) @@ -179,6 +185,7 @@ class AppModules( roleBasedHttpClientBuilder::okHttpClientForMoney, roleBasedHttpClientBuilder::shouldUseTorForMoneyOperations, otsBlockHeightCache, + customExplorerUrl = { otsPrefs.current.normalizedUrl() }, ) // Application-wide ots verification cache diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsSettings.kt new file mode 100644 index 0000000000..4b7ccb531b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/OtsSettings.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.nip03Timestamp + +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer +import kotlinx.serialization.Serializable + +/** + * Immutable data class representing the current OTS blockchain explorer config. + * + * When a custom URL is configured, it is used instead of the automatic + * Tor-aware selection (Mempool when Tor is active, Blockstream otherwise). + * This gives users control over which explorer observes their OTS verifications. + */ +@Serializable +@Stable +data class OtsSettings( + /** + * Custom blockchain explorer base API URL. + * When null/blank, the default Tor-aware selection is used. + * Must be a Mempool-compatible REST API (e.g. https://mempool.space/api/). + */ + val customExplorerUrl: String? = null, +) { + /** True when the user has configured a custom explorer URL. */ + val hasCustomExplorer: Boolean get() = !customExplorerUrl.isNullOrBlank() + + /** + * Returns the normalized custom URL (trailing slash ensured) or null if not set. + */ + fun normalizedUrl(): String? { + val url = customExplorerUrl?.trim()?.takeIf { it.isNotBlank() } ?: return null + return if (url.endsWith("/")) url else "$url/" + } + + companion object { + val DEFAULT = OtsSettings() + + val KNOWN_EXPLORERS = + listOf( + OkHttpBitcoinExplorer.MEMPOOL_API_URL to "mempool.space (Tor-friendly)", + OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL to "blockstream.info", + ) + + fun isValidUrl(url: String): Boolean { + val trimmed = url.trim() + if (trimmed.isBlank()) return false + return trimmed.startsWith("http://") || trimmed.startsWith("https://") + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt index d075edd0ae..dcf34bd96a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip03Timestamp/TorAwareOkHttpOtsResolverBuilder.kt @@ -31,13 +31,15 @@ class TorAwareOkHttpOtsResolverBuilder( val okHttpClient: (url: String) -> OkHttpClient, val isTorActive: (url: String) -> Boolean, val cache: OtsBlockHeightCache, + val customExplorerUrl: () -> String? = { null }, ) : OtsResolverBuilder { - fun getAPI(usingTor: Boolean) = - if (usingTor) { - OkHttpBitcoinExplorer.MEMPOOL_API_URL - } else { - OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL - } + fun getAPI(usingTor: Boolean): String = + customExplorerUrl() + ?: if (usingTor) { + OkHttpBitcoinExplorer.MEMPOOL_API_URL + } else { + OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL + } override fun build(): OtsResolver = OtsResolver( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/OtsSharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/OtsSharedPreferences.kt new file mode 100644 index 0000000000..315278f505 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/OtsSharedPreferences.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.model.preferences + +import android.content.Context +import androidx.compose.runtime.Stable +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsSettings +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import kotlin.coroutines.cancellation.CancellationException + +/** + * Persistent storage for [OtsSettings], following the same pattern as + * [NamecoinSharedPreferences]. + * + * Uses the app-wide [sharedPreferencesDataStore] so OTS explorer settings + * are global — not per-account. + */ +@Stable +class OtsSharedPreferences( + private val context: Context, + private val scope: CoroutineScope, +) { + companion object { + val KEY_CUSTOM_EXPLORER_URL = stringPreferencesKey("ots.customExplorerUrl") + } + + /** + * Current settings, loaded synchronously at init to avoid races. + */ + private val _settings = + MutableStateFlow( + runBlocking { loadFromDisk() ?: OtsSettings.DEFAULT }, + ) + val settings: StateFlow = _settings + + /** Synchronous snapshot — safe to call from resolver builder lambdas. */ + val current: OtsSettings get() = _settings.value + + // ── Mutators ─────────────────────────────────────────────────────── + + suspend fun setCustomExplorerUrl(url: String?) { + val normalized = url?.trim()?.takeIf { it.isNotBlank() } + persist(current.copy(customExplorerUrl = normalized)) + } + + suspend fun reset() { + persist(OtsSettings.DEFAULT) + } + + // ── Internal ─────────────────────────────────────────────────────── + + private suspend fun persist(settings: OtsSettings) { + _settings.value = settings + try { + context.sharedPreferencesDataStore.edit { prefs -> + if (settings.customExplorerUrl != null) { + prefs[KEY_CUSTOM_EXPLORER_URL] = settings.customExplorerUrl + } else { + prefs.remove(KEY_CUSTOM_EXPLORER_URL) + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("OtsPrefs", "Error writing DataStore: ${e.message}") + } + } + + private suspend fun loadFromDisk(): OtsSettings? = + try { + val prefs = context.sharedPreferencesDataStore.data.first() + val url = prefs[KEY_CUSTOM_EXPLORER_URL]?.takeIf { it.isNotBlank() } + OtsSettings(customExplorerUrl = url) + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e("OtsPrefs", "Error reading DataStore: ${e.message}") + null + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 2bb646e982..fa59d0dc45 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -112,6 +112,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ReactionsSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SecurityFiltersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsScreen @@ -177,6 +178,7 @@ fun AppNavigation( composableFromEnd { SecurityFiltersScreen(accountViewModel, nav) } composableFromEnd { PrivacyOptionsScreen(Amethyst.instance.torPrefs.value, nav) } composableFromEnd { NamecoinSettingsScreen(Amethyst.instance.namecoinPrefs, nav) } + composableFromEnd { OtsSettingsScreen(Amethyst.instance.otsPrefs, Amethyst.instance.torPrefs.value, nav) } composableFromEnd { BookmarkListScreen(accountViewModel, nav) } composableFromEnd { DraftListScreen(accountViewModel, nav) } composableFromEnd { SettingsScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index b55b4dd063..24a8e8ad4d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -51,6 +51,8 @@ sealed class Route { @Serializable object NamecoinSettings : Route() + @Serializable object OtsSettings : Route() + @Serializable object Bookmarks : Route() @Serializable object BookmarkGroups : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index 4dcf56f794..5e9e200a48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.CloudUpload import androidx.compose.material.icons.outlined.FavoriteBorder import androidx.compose.material.icons.outlined.Key +import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Security import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.ThumbUp @@ -153,6 +154,13 @@ fun AllSettingsScreen( onClick = { nav.nav(Route.NamecoinSettings) }, ) HorizontalDivider() + SettingsNavigationRow( + title = R.string.ots_explorer_settings, + icon = Icons.Outlined.Search, + tint = tint, + onClick = { nav.nav(Route.OtsSettings) }, + ) + HorizontalDivider() SettingsNavigationRow( title = R.string.ui_preferences, icon = Icons.Outlined.Settings, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsScreen.kt new file mode 100644 index 0000000000..8bbf6d743e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsScreen.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.preferences.OtsSharedPreferences +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow +import com.vitorpamplona.amethyst.ui.tor.TorType +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OtsSettingsScreen( + otsPrefs: OtsSharedPreferences, + torSettings: TorSettingsFlow, + nav: INav, +) { + val otsSettings by otsPrefs.settings.collectAsState() + val torType by torSettings.torType.collectAsState() + val moneyViaTor by torSettings.moneyOperationsViaTor.collectAsState() + val scope = rememberCoroutineScope() + + val isTorActiveForMoney = torType != TorType.OFF && moneyViaTor + + Scaffold( + topBar = { + TopBarWithBackButton(stringRes(id = R.string.ots_explorer_settings), nav::popBack) + }, + ) { + Column( + Modifier + .padding(it) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 10.dp), + ) { + OtsSettingsSection( + settings = otsSettings, + isTorActive = isTorActiveForMoney, + onSetCustomUrl = { url -> + scope.launch { otsPrefs.setCustomExplorerUrl(url) } + }, + onReset = { + scope.launch { otsPrefs.reset() } + }, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsSection.kt new file mode 100644 index 0000000000..5f2e861aba --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsSection.kt @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsSettings +import com.vitorpamplona.quartz.nip03Timestamp.okhttp.OkHttpBitcoinExplorer + +/** + * Settings section for configuring the blockchain explorer used for OTS verification. + * + * Displays the currently active explorer and allows the user to provide a + * custom Mempool-compatible REST API URL. When a custom URL is set, it takes + * priority over the automatic Tor-aware selection. + * + * @param settings Current [OtsSettings] state + * @param isTorActive Whether Tor is currently active (determines default explorer shown) + * @param onSetCustomUrl Called with the trimmed URL string when user saves a custom URL + * @param onReset Called when user clears the custom URL and reverts to auto-selection + */ +@Composable +fun OtsSettingsSection( + settings: OtsSettings, + isTorActive: Boolean, + onSetCustomUrl: (String?) -> Unit, + onReset: () -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.padding(16.dp)) { + SectionHeaderOts() + + Spacer(Modifier.height(12.dp)) + + Text( + "OpenTimestamps proofs are verified by querying a Bitcoin blockchain explorer. " + + "By default, mempool.space is used when Tor is active, and blockstream.info otherwise. " + + "Set a custom URL to use your own self-hosted instance or a trusted third party.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(Modifier.height(16.dp)) + + ActiveExplorerDisplay(settings = settings, isTorActive = isTorActive) + + Spacer(Modifier.height(12.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ) + Spacer(Modifier.height(12.dp)) + + CustomExplorerInput( + currentUrl = settings.customExplorerUrl, + onSave = onSetCustomUrl, + ) + + if (settings.hasCustomExplorer) { + Spacer(Modifier.height(4.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + ) { + TextButton(onClick = onReset) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + Spacer(Modifier.width(4.dp)) + Text("Reset to auto-select") + } + } + } + + Spacer(Modifier.height(12.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f), + ) + Spacer(Modifier.height(12.dp)) + + KnownExplorersInfo() + } +} + +// ── Sub-composables ──────────────────────────────────────────────────── + +@Composable +private fun SectionHeaderOts() { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Outlined.Search, + contentDescription = null, + tint = Color(0xFFF7931A), // Bitcoin orange + modifier = Modifier.size(22.dp), + ) + Spacer(Modifier.width(10.dp)) + Column { + Text( + "Blockchain Explorer", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.SemiBold, + ) + Text( + "Used for OTS timestamp verification", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun ActiveExplorerDisplay( + settings: OtsSettings, + isTorActive: Boolean, +) { + val isCustom = settings.hasCustomExplorer + val activeUrl = + settings.normalizedUrl() + ?: if (isTorActive) OkHttpBitcoinExplorer.MEMPOOL_API_URL else OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL + + Column { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + "Active explorer", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + ) + if (isCustom) { + Text( + "CUSTOM", + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Bold, + color = Color(0xFFF7931A), + modifier = + Modifier + .background( + Color(0xFFF7931A).copy(alpha = 0.1f), + RoundedCornerShape(4.dp), + ).padding(horizontal = 6.dp, vertical = 2.dp), + ) + } else { + Text( + if (isTorActive) "AUTO (TOR)" else "AUTO", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + Spacer(Modifier.height(6.dp)) + Text( + text = activeUrl, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface, + modifier = + Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + RoundedCornerShape(6.dp), + ).padding(horizontal = 10.dp, vertical = 6.dp), + ) + } +} + +@Composable +private fun CustomExplorerInput( + currentUrl: String?, + onSave: (String?) -> Unit, +) { + var input by rememberSaveable(currentUrl) { mutableStateOf(currentUrl ?: "") } + var validationError by remember { mutableStateOf(null) } + val kb = LocalSoftwareKeyboardController.current + + fun trySave() { + val trimmed = input.trim() + if (trimmed.isBlank()) { + // Empty input means clear the custom URL + validationError = null + onSave(null) + kb?.hide() + return + } + if (!OtsSettings.isValidUrl(trimmed)) { + validationError = "Must start with http:// or https://" + return + } + validationError = null + onSave(trimmed) + kb?.hide() + } + + Column { + Text( + "Custom explorer URL", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 6.dp), + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + ) { + OutlinedTextField( + value = input, + onValueChange = { + input = it + validationError = null + }, + label = { Text("Explorer API base URL") }, + placeholder = { Text("https://mempool.space/api/") }, + singleLine = true, + isError = validationError != null, + supportingText = + validationError?.let { err -> + { Text(err, color = MaterialTheme.colorScheme.error) } + }, + trailingIcon = + if (input.isNotBlank()) { + { + IconButton(onClick = { + input = "" + validationError = null + }) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + modifier = Modifier.size(18.dp), + ) + } + } + } else { + null + }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp), + keyboardOptions = + KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { trySave() }), + textStyle = + MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace, + ), + ) + Spacer(Modifier.width(8.dp)) + TextButton( + onClick = { trySave() }, + modifier = Modifier.padding(top = 6.dp), + ) { + Text("Save") + } + } + } +} + +@Composable +private fun KnownExplorersInfo() { + Column { + Text( + "Known compatible explorers", + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(bottom = 6.dp), + ) + OtsSettings.KNOWN_EXPLORERS.forEach { (url, label) -> + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 3.dp) + .border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f), + RoundedCornerShape(6.dp), + ).padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Medium, + ) + Text( + text = url, + style = MaterialTheme.typography.labelSmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + Spacer(Modifier.height(4.dp)) + Text( + "Any Mempool-compatible REST API is supported (e.g. self-hosted mempool.space).", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 799135f7dc..5bad7ddf91 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1748,4 +1748,5 @@ Select All %1$d%% uptime Namecoin Settings + Bitcoin Explorer (OTS) From 117c903e7ca7a8b93f34a62f5216b7bbf849da0f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Mar 2026 09:46:07 +0000 Subject: [PATCH 014/172] feat: add missing translation settings to Translation settings menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose the translateTo and languagePreferences fields from AccountLanguagePreferences in the Translation settings screen (UserSettingsScreen), which previously only showed the dontTranslateFrom setting. New settings added: - Translate To: dropdown to select the target translation language from the device's configured locales, with a checkmark on the currently selected language. - Language Display Preferences: for each saved language-pair preference (source → target), a dropdown to choose which language to show first; only visible when at least one preference exists. https://claude.ai/code/session_01BheCJbDDZDAAYo6GAJBtFf --- .../loggedIn/settings/UserSettingsScreen.kt | 180 ++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 5 + 2 files changed, 185 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index 79c685f827..3237243e68 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -20,18 +20,23 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings +import android.content.res.Resources import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField @@ -47,6 +52,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.os.ConfigurationCompat import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -54,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow @@ -88,6 +95,8 @@ fun UserSettingsScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { DontTranslateFromSetting(accountViewModel) + TranslateToSetting(accountViewModel) + LanguagePreferencesSetting(accountViewModel) } } } @@ -143,3 +152,174 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { } } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TranslateToSetting(accountViewModel: AccountViewModel) { + var expanded by remember { mutableStateOf(false) } + val currentTranslateTo = accountViewModel.translateTo() + val languageList = ConfigurationCompat.getLocales(Resources.getSystem().configuration) + + Column { + SettingsRow( + name = R.string.translate_to, + description = R.string.translate_to_description, + ) { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + ) { + OutlinedTextField( + value = JavaLocale(currentTranslateTo).displayName, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable), + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + for (i in 0 until languageList.size()) { + languageList.get(i)?.let { lang -> + DropdownMenuItem( + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (accountViewModel.account.settings.translateToContains(lang)) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) + } + Spacer(modifier = Modifier.size(10.dp)) + Text(text = lang.displayName) + } + }, + onClick = { + accountViewModel.updateTranslateTo(lang) + expanded = false + }, + ) + } + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) { + val languagePreferences = accountViewModel.account.settings.syncedSettings.languages.languagePreferences + + if (languagePreferences.isEmpty()) return + + Column { + SettingsRow( + name = R.string.language_preferences, + description = R.string.language_preferences_description, + ) {} + + languagePreferences.forEach { (key, preference) -> + val parts = key.split(",") + if (parts.size == 2) { + LanguagePreferenceItem( + source = parts[0], + target = parts[1], + preference = preference, + accountViewModel = accountViewModel, + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LanguagePreferenceItem( + source: String, + target: String, + preference: String, + accountViewModel: AccountViewModel, +) { + val sourceName = JavaLocale(source).displayName + val targetName = JavaLocale(target).displayName + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.padding(vertical = 4.dp), + ) { + OutlinedTextField( + value = stringRes(R.string.language_preference_pair, sourceName, targetName), + onValueChange = {}, + readOnly = true, + label = { + Text( + if (preference == source) { + stringRes(R.string.translations_show_in_lang_first, sourceName) + } else { + stringRes(R.string.translations_show_in_lang_first, targetName) + }, + ) + }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable), + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + DropdownMenuItem( + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (preference == source) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) + } + Spacer(modifier = Modifier.size(10.dp)) + Text(stringRes(R.string.translations_show_in_lang_first, sourceName)) + } + }, + onClick = { + accountViewModel.prefer(source, target, source) + expanded = false + }, + ) + HorizontalDivider(thickness = DividerThickness) + DropdownMenuItem( + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (preference == target) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) + } + Spacer(modifier = Modifier.size(10.dp)) + Text(stringRes(R.string.translations_show_in_lang_first, targetName)) + } + }, + onClick = { + accountViewModel.prefer(source, target, target) + expanded = false + }, + ) + } + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 799135f7dc..3ec7aaf359 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1500,6 +1500,11 @@ Don\'t Translate From Languages shown here will not be translated. Select a language to remove it and have it translated again. + Translate To + Choose the language to translate content into. + Language Display Preferences + For each translated language pair, choose which language to show first. + %1$s → %2$s Pause Play Open dropdown menu From e07d42e3dad525278def87ca270c97ebc25af417 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Mar 2026 10:38:23 +0000 Subject: [PATCH 015/172] feat: add NIP-40 expiration date to short note post screen Allows users to add an expiration date to posts via NIP-40. The feature follows the same pattern as content warning (toggle button in the bottom action bar, expandable section in the post body) and uses the same calendar/time picker UI as poll deadlines. - ExpirationDateButton: timer icon toggles orange when active - ExpirationDatePicker: date+time picker with "Expires in X" display - ViewModel: wantsExpirationDate + expirationDate state, wired into both TextNoteEvent and PollEvent builders, loaded from drafts - Strings: add_expiration_date, expiration_date_label, etc. https://claude.ai/code/session_016QjmArr6zfM1Qx58iNrntA --- .../expiration/ExpirationDateButton.kt | 61 ++++++ .../expiration/ExpirationDatePicker.kt | 195 ++++++++++++++++++ .../loggedIn/home/ShortNotePostScreen.kt | 15 ++ .../loggedIn/home/ShortNotePostViewModel.kt | 24 +++ amethyst/src/main/res/values/strings.xml | 8 + 5 files changed, 303 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDateButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDateButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDateButton.kt new file mode 100644 index 0000000000..487662291c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDateButton.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.creators.expiration + +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material.icons.outlined.TimerOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes + +@Composable +fun ExpirationDateButton( + isActive: Boolean, + onClick: () -> Unit, +) { + IconButton( + onClick = { onClick() }, + ) { + if (!isActive) { + Icon( + imageVector = Icons.Outlined.Timer, + contentDescription = stringRes(R.string.add_expiration_date), + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onBackground, + ) + } else { + Icon( + imageVector = Icons.Outlined.TimerOff, + contentDescription = stringRes(R.string.remove_expiration_date), + modifier = Modifier.size(20.dp), + tint = Color(0xFFFF6600), + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt new file mode 100644 index 0000000000..5dcd18b0a0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.creators.expiration + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SelectableDates +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker +import androidx.compose.material3.TimePickerDialog +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.utils.TimeUtils +import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ExpirationDatePicker(model: ShortNotePostViewModel) { + var showDatePicker by remember { mutableStateOf(false) } + var showTimePicker by remember { mutableStateOf(false) } + + val currentTime = Instant.ofEpochMilli(model.expirationDate * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime() + + val datePickerState = + rememberDatePickerState( + initialSelectedDateMillis = model.expirationDate * 1000, + yearRange = currentTime.year..2050, + selectableDates = + object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + return utcTimeMillis >= System.currentTimeMillis() - 86400000 + } + }, + ) + + val timePickerState = + rememberTimePickerState( + initialHour = currentTime.hour, + initialMinute = currentTime.minute, + is24Hour = false, + ) + + val context = LocalContext.current + + Column(Modifier.fillMaxWidth()) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .fillMaxWidth() + .padding(bottom = 10.dp), + ) { + Icon( + imageVector = Icons.Outlined.Timer, + contentDescription = stringRes(R.string.expiration_date_label), + modifier = Modifier.size(20.dp), + tint = Color(0xFFFF6600), + ) + + Text( + text = stringRes(R.string.expiration_date_label), + fontSize = 20.sp, + fontWeight = FontWeight.W500, + modifier = Modifier.padding(start = 10.dp), + ) + } + + HorizontalDivider(thickness = DividerThickness) + + Text( + text = stringRes(R.string.expiration_date_explainer), + color = MaterialTheme.colorScheme.placeholderText, + modifier = Modifier.padding(vertical = 10.dp), + ) + + OutlinedCard( + onClick = { showDatePicker = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon(Icons.Outlined.Timer, contentDescription = stringResource(R.string.expiration_date_select)) + Spacer(Modifier.width(12.dp)) + + if (model.expirationDate < TimeUtils.oneMinuteFromNow()) { + Text(stringRes(R.string.expiration_date_label) + " " + model.expirationDate, style = MaterialTheme.typography.bodyLarge) + } else { + Text( + text = stringRes(R.string.expiration_expires_in, timeAheadNoDot(model.expirationDate, context)), + style = MaterialTheme.typography.bodyLarge, + ) + } + } + } + } + + if (showDatePicker) { + DatePickerDialog( + onDismissRequest = { showDatePicker = false }, + confirmButton = { + TextButton(onClick = { + showDatePicker = false + showTimePicker = true + }) { Text(stringResource(R.string.next)) } + }, + ) { + DatePicker(state = datePickerState) + } + } + + if (showTimePicker) { + TimePickerDialog( + title = { + Text(stringResource(R.string.expiration_time)) + }, + onDismissRequest = { showTimePicker = false }, + confirmButton = { + TextButton( + onClick = { + val datetimeLocalTimeZone = + datePickerState.selectedDateMillis?.let { localDayAtZeroHourMillis -> + (localDayAtZeroHourMillis / 1000) + + (timePickerState.hour * TimeUtils.ONE_HOUR) + + (timePickerState.minute * TimeUtils.ONE_MINUTE) + } ?: TimeUtils.oneDayAhead() + + val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) + + model.expirationDate = datetimeLocalTimeZone - offset.totalSeconds + + showTimePicker = false + }, + ) { Text(stringResource(R.string.confirm)) } + }, + ) { + TimePicker(state = timePickerState) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 99f8a6e98f..6432d21691 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -78,6 +78,8 @@ import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton @@ -311,6 +313,15 @@ private fun NewPostScreenBody( } } + if (postViewModel.wantsExpirationDate) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + ExpirationDatePicker(postViewModel) + } + } + if (postViewModel.wantsToAddGeoHash) { Row( verticalAlignment = CenterVertically, @@ -539,6 +550,10 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 5eb51fd1ae..e07e0141d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -97,6 +97,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress @@ -239,6 +240,10 @@ open class ShortNotePostViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -427,6 +432,10 @@ open class ShortNotePostViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.tags.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null if (geohash != null) { @@ -507,6 +516,10 @@ open class ShortNotePostViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.tags.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null if (geohash != null) { @@ -690,6 +703,7 @@ open class ShortNotePostViewModel : val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null return if (wantsPoll) { val options = pollOptions.map { it.value } @@ -710,6 +724,7 @@ open class ShortNotePostViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -765,6 +780,7 @@ open class ShortNotePostViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -1237,5 +1253,13 @@ open class ShortNotePostViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + override fun locationManager(): LocationState = Amethyst.instance.locationManager } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 799135f7dc..69eef425e3 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1291,6 +1291,14 @@ Add content warning Remove content warning + + Add expiration date + Remove expiration date + Expiration Date + Post will be hidden by clients after this date (NIP-40) + Select expiration date and time + Expires in %1$s + Expiration Time Show npub as a QR code Show nprofile as a QR code From 5a437ed5ac22999c56e10ff2d61a9d8e5989acda Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 12 Mar 2026 13:23:27 +0200 Subject: [PATCH 016/172] feat(search): collapsible section headers + sorting + search improvements - Collapsible sticky section headers with animated chevron - Full header row clickable to toggle collapse - SearchResultSorter, SearchSortOrder, pseudo-kind filtering - TextFieldValue cursor preservation, relay timeout improvements Co-Authored-By: Claude Opus 4.6 --- .../settings/ReactionsSettingsScreen.kt | 76 +- .../commons/search/AdvancedSearchBarState.kt | 35 +- .../amethyst/commons/search/KindRegistry.kt | 30 +- .../commons/search/SearchResultSorter.kt | 117 +++ .../commons/search/SearchSortOrder.kt | 39 + .../commons/search/SearchResultSorterTest.kt | 197 ++++ .../amethyst/desktop/ui/SearchScreen.kt | 35 +- .../desktop/ui/search/AdvancedSearchPanel.kt | 78 +- .../desktop/ui/search/SearchResultsList.kt | 187 +++- .../2026-03-10-advanced-search-brainstorm.md | 230 +++++ ...03-10-feat-desktop-advanced-search-plan.md | 962 ++++++++++++++++++ 11 files changed, 1849 insertions(+), 137 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSortOrder.kt create mode 100644 commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorterTest.kt create mode 100644 docs/brainstorms/2026-03-10-advanced-search-brainstorm.md create mode 100644 docs/plans/2026-03-10-feat-desktop-advanced-search-plan.md diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt index e995b13772..15b5e583c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt @@ -21,10 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -33,9 +31,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DragIndicator +import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -57,32 +56,17 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.ReactionRowAction import com.vitorpamplona.amethyst.model.ReactionRowItem -import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow - -@Composable -@Preview(device = "spec:width=2100px,height=2340px,dpi=440") -fun ReactionsSettingsScreenPreview() { - ThemeComparisonRow { - ReactionsSettingsScreen( - mockAccountViewModel(), - EmptyNav(), - ) - } -} @Composable fun ReactionsSettingsScreen( @@ -103,7 +87,7 @@ fun ReactionsSettingsScreen( @Composable fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { val reactionRowItems by accountViewModel.reactionRowItemsFlow().collectAsStateWithLifecycle() - var items by remember(reactionRowItems) { mutableStateOf(reactionRowItems.toList()) } + var items by remember(reactionRowItems) { mutableStateOf(reactionRowItems.toMutableList()) } fun save(newItems: List) { items = newItems.toMutableList() @@ -113,14 +97,13 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { var draggedItemIndex by remember { mutableIntStateOf(-1) } var dragOffset by remember { mutableFloatStateOf(0f) } val itemHeights = remember { mutableStateMapOf() } - val isDragging = draggedItemIndex >= 0 - val scrollState = remember { ScrollState(0) } Column( modifier = Modifier .fillMaxSize() - .verticalScroll(scrollState, enabled = !isDragging), + .padding(horizontal = Size20dp) + .verticalScroll(rememberScrollState()), ) { Spacer(modifier = Modifier.height(16.dp)) @@ -128,7 +111,7 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { text = stringRes(R.string.reactions_settings_description), style = MaterialTheme.typography.bodyMedium, color = Color.Gray, - modifier = Modifier.padding(bottom = 16.dp, start = Size20dp, end = Size20dp), + modifier = Modifier.padding(bottom = 16.dp), ) items.forEachIndexed { index, item -> @@ -223,7 +206,7 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { .zIndex(if (isDragging) 1f else 0f), ) if (index < items.lastIndex) { - HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp)) + HorizontalDivider() } } @@ -262,18 +245,7 @@ private fun ReactionRowItemCard( scaleX = 1.02f scaleY = 1.02f } - }.padding(vertical = 8.dp, horizontal = Size20dp) - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { onDragStart() }, - onDrag = { change, dragAmount -> - change.consume() - onDrag(dragAmount.y) - }, - onDragEnd = { onDragEnd() }, - onDragCancel = { onDragCancel() }, - ) - }, + }.padding(vertical = 8.dp), ) { Row( modifier = Modifier.fillMaxWidth(), @@ -296,17 +268,25 @@ private fun ReactionRowItemCard( ) } - Box( - modifier = Modifier.size(32.dp), - contentAlignment = Alignment.CenterEnd, - ) { - Icon( - Icons.Default.DragIndicator, - contentDescription = stringRes(R.string.reactions_settings_reorder), - modifier = Modifier.size(28.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + Icon( + Icons.Default.DragHandle, + contentDescription = stringRes(R.string.reactions_settings_reorder), + modifier = + Modifier + .size(32.dp) + .pointerInput(Unit) { + detectDragGesturesAfterLongPress( + onDragStart = { onDragStart() }, + onDrag = { change, dragAmount -> + change.consume() + onDrag(dragAmount.y) + }, + onDragEnd = { onDragEnd() }, + onDragCancel = { onDragCancel() }, + ) + }, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) } Spacer(modifier = Modifier.height(8.dp)) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt index 5a375ebc96..88702439c3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/AdvancedSearchBarState.kt @@ -83,6 +83,24 @@ class AdvancedSearchBarState( private val _noteResults = MutableStateFlow>(persistentListOf()) val noteResults: StateFlow> = _noteResults.asStateFlow() + // Sort orders + private val _eventSortOrder = MutableStateFlow(SearchSortOrder.DEFAULT_EVENT) + val eventSortOrder: StateFlow = _eventSortOrder.asStateFlow() + + private val _peopleSortOrder = MutableStateFlow(SearchSortOrder.DEFAULT_PEOPLE) + val peopleSortOrder: StateFlow = _peopleSortOrder.asStateFlow() + + // Derived sorted results + val sortedNoteResults: StateFlow> = + combine(_noteResults, _eventSortOrder, _rawText) { notes, order, text -> + SearchResultSorter.sortEvents(notes, order, text).toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + + val sortedPeopleResults: StateFlow> = + combine(_peopleResults, _peopleSortOrder) { people, order -> + SearchResultSorter.sortPeople(people, order).toImmutableList() + }.stateIn(scope, SharingStarted.Eagerly, persistentListOf()) + private val activeSubIds = MutableStateFlow>(emptySet()) val isSearching: StateFlow = activeSubIds @@ -112,6 +130,11 @@ class AdvancedSearchBarState( _query.value = _query.value.copy(kinds = kinds.toImmutableList()) } + fun updatePseudoKinds(pseudoKinds: List) { + _changeSource = ChangeSource.FORM + _query.value = _query.value.copy(pseudoKinds = pseudoKinds.toImmutableList()) + } + fun addAuthor(hexOrName: String) { _changeSource = ChangeSource.FORM val current = _query.value @@ -228,6 +251,14 @@ class AdvancedSearchBarState( _panelExpanded.value = !_panelExpanded.value } + fun updateEventSortOrder(order: SearchSortOrder) { + _eventSortOrder.value = order + } + + fun updatePeopleSortOrder(order: SearchSortOrder) { + _peopleSortOrder.value = order + } + fun clearSearch() { _changeSource = ChangeSource.INIT _rawText.value = "" @@ -235,6 +266,8 @@ class AdvancedSearchBarState( _peopleResults.value = persistentListOf() _noteResults.value = persistentListOf() _relayStates.value = persistentListOf() + _eventSortOrder.value = SearchSortOrder.DEFAULT_EVENT + _peopleSortOrder.value = SearchSortOrder.DEFAULT_PEOPLE activeSubIds.value = emptySet() eventDeduplicator.clear() } @@ -275,7 +308,7 @@ class AdvancedSearchBarState( fun addNoteResults(events: List) { if (events.isNotEmpty()) { val current = _noteResults.value - _noteResults.value = (current + events).sortedByDescending { it.createdAt }.toImmutableList() + _noteResults.value = (current + events).toImmutableList() } } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt index 2b3e921b7f..e914be2941 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/KindRegistry.kt @@ -32,6 +32,22 @@ import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefiniti import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +data class ContentPreset( + val kinds: List = emptyList(), + val pseudoKind: String? = null, +) { + /** Check if this preset is active given current query state. */ + fun isSelected( + queryKinds: List, + queryPseudoKinds: List, + ): Boolean = + if (pseudoKind != null) { + pseudoKind in queryPseudoKinds + } else { + kinds.isNotEmpty() && queryKinds.containsAll(kinds) + } +} + object KindRegistry { val aliases: Map> = mapOf( @@ -49,14 +65,14 @@ object KindRegistry { val pseudoKinds: Set = setOf("reply", "media") - val presets: Map> = + val presets: Map = mapOf( - "Notes" to listOf(TextNoteEvent.KIND), - "Articles" to listOf(LongTextNoteEvent.KIND), - "Media" to listOf(TextNoteEvent.KIND), - "Channels" to listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND), - "Communities" to listOf(CommunityDefinitionEvent.KIND), - "Wiki" to listOf(WikiNoteEvent.KIND), + "Notes" to ContentPreset(kinds = listOf(TextNoteEvent.KIND)), + "Articles" to ContentPreset(kinds = listOf(LongTextNoteEvent.KIND)), + "Media" to ContentPreset(pseudoKind = "media"), + "Channels" to ContentPreset(kinds = listOf(ChannelCreateEvent.KIND, ChannelMetadataEvent.KIND)), + "Communities" to ContentPreset(kinds = listOf(CommunityDefinitionEvent.KIND)), + "Wiki" to ContentPreset(kinds = listOf(WikiNoteEvent.KIND)), ) fun resolve(alias: String): List? = aliases[alias.lowercase()] diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt new file mode 100644 index 0000000000..bc1ea81770 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorter.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.utils.currentTimeSeconds + +object SearchResultSorter { + fun sortEvents( + events: List, + order: SearchSortOrder, + searchText: String, + ): List = + when (order) { + SearchSortOrder.NEWEST -> { + events.sortedByDescending { it.createdAt } + } + + SearchSortOrder.OLDEST -> { + events.sortedBy { it.createdAt } + } + + SearchSortOrder.RELEVANCE -> { + if (searchText.isBlank()) { + events.sortedByDescending { it.createdAt } + } else { + events.sortedByDescending { scoreEvent(it, searchText) } + } + } + + else -> { + events + } + } + + fun sortPeople( + people: List, + order: SearchSortOrder, + ): List = + when (order) { + SearchSortOrder.NAME_AZ -> people.sortedBy { it.toBestDisplayName().lowercase() } + SearchSortOrder.NAME_ZA -> people.sortedByDescending { it.toBestDisplayName().lowercase() } + else -> people + } + + fun scoreEvent( + event: Event, + searchText: String, + ): Double { + val query = searchText.trim().lowercase() + if (query.isEmpty()) return event.createdAt.toDouble() + + var score = 0.0 + val content = event.content.lowercase() + val tokens = query.split("\\s+".toRegex()) + + // Exact phrase match in content + if (content.contains(query)) { + score += 10.0 + } + + // Per-token scoring + for (token in tokens) { + if (token.isEmpty()) continue + val wordBoundary = "\\b${Regex.escape(token)}\\b".toRegex() + if (wordBoundary.containsMatchIn(content)) { + score += 5.0 + } else if (content.contains(token)) { + score += 2.0 + } + } + + // Article title boost + if (event is LongTextNoteEvent) { + val title = event.title()?.lowercase() + if (title != null) { + if (title.contains(query)) { + score += 15.0 + } + for (token in tokens) { + if (token.isEmpty()) continue + if (title.contains(token)) { + score += 3.0 + } + } + } + } + + // Recency tiebreaker (normalized 0..1) + val now = currentTimeSeconds() + val age = (now - event.createdAt).coerceAtLeast(1) + val maxAge = 365L * 24 * 3600 // 1 year + score += (1.0 - (age.toDouble() / maxAge).coerceIn(0.0, 1.0)) + + return score + } +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSortOrder.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSortOrder.kt new file mode 100644 index 0000000000..1b43507870 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/search/SearchSortOrder.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +enum class SearchSortOrder( + val label: String, +) { + NEWEST("Newest"), + OLDEST("Oldest"), + RELEVANCE("Relevance"), + NAME_AZ("A → Z"), + NAME_ZA("Z → A"), + ; + + companion object { + val EVENT_OPTIONS = listOf(NEWEST, OLDEST, RELEVANCE) + val PEOPLE_OPTIONS = listOf(NAME_AZ, NAME_ZA) + val DEFAULT_EVENT = NEWEST + val DEFAULT_PEOPLE = NAME_AZ + } +} diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorterTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorterTest.kt new file mode 100644 index 0000000000..bdf3b3cf6e --- /dev/null +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/SearchResultSorterTest.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.search + +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent +import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SearchResultSorterTest { + private fun event( + id: String, + createdAt: Long, + content: String = "", + kind: Int = 1, + ) = Event( + id = id, + pubKey = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd", + createdAt = createdAt, + kind = kind, + tags = emptyArray(), + content = content, + sig = "sig", + ) + + private fun article( + id: String, + createdAt: Long, + content: String = "", + title: String? = null, + ): LongTextNoteEvent { + val tags = + if (title != null) { + arrayOf(arrayOf("title", title)) + } else { + emptyArray() + } + return LongTextNoteEvent( + id = id, + pubKey = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd", + createdAt = createdAt, + tags = tags, + content = content, + sig = "sig", + ) + } + + private fun user( + hex: String, + displayName: String, + ): User { + val u = User(hex, Note("r1-$hex"), Note("r2-$hex")) + val meta = UserMetadata().apply { this.displayName = displayName } + val metaEvent = + MetadataEvent( + id = "meta-$hex", + pubKey = hex, + createdAt = 0L, + tags = emptyArray(), + content = "{}", + sig = "sig", + ) + u.updateUserInfo(meta, metaEvent) + return u + } + + // --- Event sorting --- + + @Test + fun newestSortsDescending() { + val events = listOf(event("a", 100), event("b", 300), event("c", 200)) + val sorted = SearchResultSorter.sortEvents(events, SearchSortOrder.NEWEST, "") + assertEquals(listOf("b", "c", "a"), sorted.map { it.id }) + } + + @Test + fun oldestSortsAscending() { + val events = listOf(event("a", 300), event("b", 100), event("c", 200)) + val sorted = SearchResultSorter.sortEvents(events, SearchSortOrder.OLDEST, "") + assertEquals(listOf("b", "c", "a"), sorted.map { it.id }) + } + + @Test + fun relevanceEmptyQueryFallsBackToRecency() { + val events = listOf(event("a", 100), event("b", 300), event("c", 200)) + val sorted = SearchResultSorter.sortEvents(events, SearchSortOrder.RELEVANCE, "") + assertEquals(listOf("b", "c", "a"), sorted.map { it.id }) + } + + @Test + fun relevanceExactMatchBeatsPartial() { + val exact = event("exact", 100, content = "bitcoin is great") + val partial = event("partial", 100, content = "bit of something") + val sorted = SearchResultSorter.sortEvents(listOf(partial, exact), SearchSortOrder.RELEVANCE, "bitcoin") + assertEquals("exact", sorted.first().id) + } + + @Test + fun relevanceWordBoundaryBeatsSubstring() { + val boundary = event("boundary", 100, content = "I love bitcoin and lightning") + val substring = event("substr", 100, content = "bitcoinery is not a word") + val sorted = SearchResultSorter.sortEvents(listOf(substring, boundary), SearchSortOrder.RELEVANCE, "bitcoin") + assertEquals("boundary", sorted.first().id) + } + + @Test + fun relevanceArticleTitleBoost() { + val withTitle = article("titled", 100, content = "some content", title = "Bitcoin Guide") + val withoutTitle = event("notitle", 100, content = "bitcoin bitcoin bitcoin") + val sorted = SearchResultSorter.sortEvents(listOf(withoutTitle, withTitle), SearchSortOrder.RELEVANCE, "bitcoin") + assertEquals("titled", sorted.first().id) + } + + @Test + fun relevanceMultipleTokensAddUp() { + val multi = event("multi", 100, content = "bitcoin and lightning network") + val single = event("single", 100, content = "bitcoin only here") + val sorted = + SearchResultSorter.sortEvents( + listOf(single, multi), + SearchSortOrder.RELEVANCE, + "bitcoin lightning", + ) + assertEquals("multi", sorted.first().id) + } + + // --- People sorting --- + + @Test + fun nameAzSortsAlphabetically() { + val people = + listOf( + user("cc00000000000000000000000000000000000000000000000000000000000000", "Charlie"), + user("aa00000000000000000000000000000000000000000000000000000000000000", "Alice"), + user("bb00000000000000000000000000000000000000000000000000000000000000", "Bob"), + ) + val sorted = SearchResultSorter.sortPeople(people, SearchSortOrder.NAME_AZ) + assertEquals(listOf("Alice", "Bob", "Charlie"), sorted.map { it.toBestDisplayName() }) + } + + @Test + fun nameZaSortsReverseAlphabetically() { + val people = + listOf( + user("aa00000000000000000000000000000000000000000000000000000000000000", "Alice"), + user("cc00000000000000000000000000000000000000000000000000000000000000", "Charlie"), + user("bb00000000000000000000000000000000000000000000000000000000000000", "Bob"), + ) + val sorted = SearchResultSorter.sortPeople(people, SearchSortOrder.NAME_ZA) + assertEquals(listOf("Charlie", "Bob", "Alice"), sorted.map { it.toBestDisplayName() }) + } + + @Test + fun nameSortIsCaseInsensitive() { + val people = + listOf( + user("aa00000000000000000000000000000000000000000000000000000000000000", "alice"), + user("bb00000000000000000000000000000000000000000000000000000000000000", "Bob"), + ) + val sorted = SearchResultSorter.sortPeople(people, SearchSortOrder.NAME_AZ) + assertEquals("alice", sorted.first().toBestDisplayName()) + } + + // --- Score function --- + + @Test + fun scoreExactPhraseHigherThanTokens() { + val exactEvent = event("e", 100, content = "bitcoin lightning network") + val tokenEvent = event("t", 100, content = "lightning and bitcoin elsewhere network") + val exactScore = SearchResultSorter.scoreEvent(exactEvent, "bitcoin lightning") + val tokenScore = SearchResultSorter.scoreEvent(tokenEvent, "bitcoin lightning") + assertTrue(exactScore > tokenScore) + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index ba29094af4..da405b6c4a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -64,8 +64,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -131,6 +133,14 @@ fun SearchScreen( val relayStatuses by relayManager.relayStatuses.collectAsState() val allRelayUrls = remember(relayStatuses) { relayStatuses.keys } val displayText by state.displayText.collectAsState() + // Track TextFieldValue locally to preserve cursor position + var textFieldValue by remember { mutableStateOf(TextFieldValue(displayText)) } + // Sync from flow only when text changes externally (form-driven updates) + LaunchedEffect(displayText) { + if (textFieldValue.text != displayText) { + textFieldValue = TextFieldValue(text = displayText, selection = TextRange(displayText.length)) + } + } val query by state.query.collectAsState() val debouncedQuery by state.debouncedQuery.collectAsState() val panelExpanded by state.panelExpanded.collectAsState() @@ -142,12 +152,19 @@ fun SearchScreen( // Bech32 parsing (immediate, no debounce) val bech32Results = remember(displayText) { parseSearchInput(displayText) } + // Skip people search when query specifies kinds that don't include profile (kind 0) + val shouldSearchPeople = + (debouncedQuery.kinds.isEmpty() && debouncedQuery.pseudoKinds.isEmpty()) || + debouncedQuery.kinds.contains(MetadataEvent.KIND) + // Clear results and start loading when query changes LaunchedEffect(debouncedQuery) { if (!debouncedQuery.isEmpty && bech32Results.isEmpty()) { state.clearResults() state.initRelayStates(allRelayUrls) - state.startSearching("people-search") + if (shouldSearchPeople) { + state.startSearching("people-search") + } state.startSearching("adv-search") // Timeout relays that silently ignore NIP-50 (e.g. strfry) kotlinx.coroutines.delay(10_000L) @@ -161,6 +178,10 @@ fun SearchScreen( return@rememberSubscription null } if (bech32Results.isNotEmpty()) return@rememberSubscription null + if (!shouldSearchPeople) { + state.stopSearching("people-search") + return@rememberSubscription null + } createSearchPeopleSubscription( relays = allRelayUrls, @@ -334,12 +355,11 @@ fun SearchScreen( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { OutlinedTextField( - value = - TextFieldValue( - text = displayText, - selection = TextRange(displayText.length), - ), - onValueChange = { state.updateFromText(it.text) }, + value = textFieldValue, + onValueChange = { + textFieldValue = it + state.updateFromText(it.text) + }, modifier = Modifier.weight(1f).focusRequester(focusRequester), placeholder = { Text("Search notes, people, tags... or use operators") }, leadingIcon = { @@ -386,6 +406,7 @@ fun SearchScreen( AdvancedSearchPanel( query = query, onKindsChanged = { state.updateKinds(it) }, + onPseudoKindsChanged = { state.updatePseudoKinds(it) }, onAuthorAdded = { state.addAuthor(it) }, onAuthorRemoved = { state.removeAuthor(it) }, onDateRangeChanged = { since, until -> state.updateDateRange(since, until) }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt index ec1eeae199..4b18918c42 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/AdvancedSearchPanel.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.search.ContentPreset import com.vitorpamplona.amethyst.commons.search.DateUtils import com.vitorpamplona.amethyst.commons.search.KindRegistry import com.vitorpamplona.amethyst.commons.search.QueryParser @@ -66,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.search.SearchQuery fun AdvancedSearchPanel( query: SearchQuery, onKindsChanged: (List) -> Unit, + onPseudoKindsChanged: (List) -> Unit, onAuthorAdded: (String) -> Unit, onAuthorRemoved: (String) -> Unit, onDateRangeChanged: (Long?, Long?) -> Unit, @@ -99,10 +101,10 @@ fun AdvancedSearchPanel( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - KindRegistry.presets.forEach { (name, kinds) -> + KindRegistry.presets.forEach { (name, preset) -> FilterChip( - selected = query.kinds.toList().containsAll(kinds), - onClick = { onKindsChanged(toggleKinds(query.kinds.toList(), kinds)) }, + selected = preset.isSelected(query.kinds.toList(), query.pseudoKinds.toList()), + onClick = { togglePreset(preset, query, onKindsChanged, onPseudoKindsChanged) }, label = { Text(name) }, ) } @@ -161,15 +163,29 @@ fun AdvancedSearchPanel( } } -private fun toggleKinds( - current: List, - toggle: List, -): List = - if (current.containsAll(toggle)) { - current - toggle.toSet() +private fun togglePreset( + preset: ContentPreset, + query: SearchQuery, + onKindsChanged: (List) -> Unit, + onPseudoKindsChanged: (List) -> Unit, +) { + val pseudo = preset.pseudoKind + if (pseudo != null) { + val current = query.pseudoKinds.toList() + if (pseudo in current) { + onPseudoKindsChanged(current - pseudo) + } else { + onPseudoKindsChanged(current + pseudo) + } } else { - (current + toggle).distinct() + val current = query.kinds.toList() + if (current.containsAll(preset.kinds)) { + onKindsChanged(current - preset.kinds.toSet()) + } else { + onKindsChanged((current + preset.kinds).distinct()) + } } +} @OptIn(ExperimentalLayoutApi::class) @Composable @@ -242,19 +258,21 @@ private fun DateRangeFields( until: Long?, onChanged: (Long?, Long?) -> Unit, ) { - var sinceText by remember(since) { - mutableStateOf( - since?.let { - DateUtils.timestampToDate(it) - } ?: "", - ) + // Local text is source of truth while typing. + // Only propagate valid timestamps (or null when cleared). + // Only sync from external when the timestamp changes to something we didn't produce. + var sinceText by remember { mutableStateOf(since?.let { DateUtils.timestampToDate(it) } ?: "") } + var lastSince by remember { mutableStateOf(since) } + if (since != lastSince) { + sinceText = since?.let { DateUtils.timestampToDate(it) } ?: "" + lastSince = since } - var untilText by remember(until) { - mutableStateOf( - until?.let { - DateUtils.timestampToDate(it) - } ?: "", - ) + + var untilText by remember { mutableStateOf(until?.let { DateUtils.timestampToDate(it) } ?: "") } + var lastUntil by remember { mutableStateOf(until) } + if (until != lastUntil) { + untilText = until?.let { DateUtils.timestampToDate(it) } ?: "" + lastUntil = until } Row( @@ -271,9 +289,11 @@ private fun DateRangeFields( value = sinceText, onValueChange = { sinceText = it - val ts = - QueryParser.parseDateToTimestamp(it) - onChanged(ts, until) + val ts = QueryParser.parseDateToTimestamp(it) + if (ts != null || it.isBlank()) { + lastSince = ts + onChanged(ts, until) + } }, modifier = Modifier.fillMaxWidth(), placeholder = { Text("YYYY-MM-DD") }, @@ -290,9 +310,11 @@ private fun DateRangeFields( value = untilText, onValueChange = { untilText = it - val ts = - QueryParser.parseDateToTimestamp(it) - onChanged(since, ts) + val ts = QueryParser.parseDateToTimestamp(it) + if (ts != null || it.isBlank()) { + lastUntil = ts + onChanged(since, ts) + } }, modifier = Modifier.fillMaxWidth(), placeholder = { Text("YYYY-MM-DD") }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt index b5bce1111f..bb43400f96 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.desktop.ui.search +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -41,6 +42,8 @@ import androidx.compose.material.icons.filled.Forum import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -50,17 +53,20 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.search.AdvancedSearchBarState import com.vitorpamplona.amethyst.commons.search.KindRegistry +import com.vitorpamplona.amethyst.commons.search.SearchSortOrder import com.vitorpamplona.amethyst.commons.ui.components.UserSearchCard import com.vitorpamplona.amethyst.commons.util.toTimeAgo import com.vitorpamplona.quartz.nip01Core.core.Event @@ -74,8 +80,10 @@ fun SearchResultsList( modifier: Modifier = Modifier, listState: LazyListState = rememberLazyListState(), ) { - val people by state.peopleResults.collectAsState() - val notes by state.noteResults.collectAsState() + val people by state.sortedPeopleResults.collectAsState() + val notes by state.sortedNoteResults.collectAsState() + val eventSortOrder by state.eventSortOrder.collectAsState() + val peopleSortOrder by state.peopleSortOrder.collectAsState() val hasResults = people.isNotEmpty() || notes.isNotEmpty() @@ -86,6 +94,9 @@ fun SearchResultsList( val articles = notes.filter { it.kind == LongTextNoteEvent.KIND } val otherNotes = notes.filter { it.kind != 1 && it.kind != LongTextNoteEvent.KIND } + // Per-section collapsed state (absent = expanded) + val collapsedSections = remember { mutableStateMapOf() } + LazyColumn( state = listState, verticalArrangement = Arrangement.spacedBy(8.dp), @@ -93,25 +104,37 @@ fun SearchResultsList( ) { // People section if (people.isNotEmpty()) { + val collapsed = collapsedSections["people"] == true stickyHeader(key = "header-people") { - SectionHeader("People", people.size, Icons.Default.Person) - } - val displayPeople = people.take(5) - items(displayPeople, key = { "person-${it.pubkeyHex}" }) { user -> - UserSearchCard( - user = user, - onClick = { onNavigateToProfile(user.pubkeyHex) }, + SortableHeader( + title = "People", + count = people.size, + icon = Icons.Default.Person, + options = SearchSortOrder.PEOPLE_OPTIONS, + selected = peopleSortOrder, + onSelect = { state.updatePeopleSortOrder(it) }, + collapsed = collapsed, + onToggleCollapse = { collapsedSections["people"] = !collapsed }, ) } - if (people.size > 5) { - item(key = "people-expand") { - ExpandableSection( - remaining = people.drop(5), - ) { user -> - UserSearchCard( - user = user, - onClick = { onNavigateToProfile(user.pubkeyHex) }, - ) + if (!collapsed) { + val displayPeople = people.take(5) + items(displayPeople, key = { "person-${it.pubkeyHex}" }) { user -> + UserSearchCard( + user = user, + onClick = { onNavigateToProfile(user.pubkeyHex) }, + ) + } + if (people.size > 5) { + item(key = "people-expand") { + ExpandableSection( + remaining = people.drop(5), + ) { user -> + UserSearchCard( + user = user, + onClick = { onNavigateToProfile(user.pubkeyHex) }, + ) + } } } } @@ -122,19 +145,31 @@ fun SearchResultsList( if (people.isNotEmpty()) { item(key = "divider-notes") { HorizontalDivider(Modifier.padding(vertical = 4.dp)) } } + val collapsed = collapsedSections["notes"] == true stickyHeader(key = "header-notes") { - SectionHeader("Notes", textNotes.size, Icons.Default.Description) + SortableHeader( + title = "Notes", + count = textNotes.size, + icon = Icons.Default.Description, + options = SearchSortOrder.EVENT_OPTIONS, + selected = eventSortOrder, + onSelect = { state.updateEventSortOrder(it) }, + collapsed = collapsed, + onToggleCollapse = { collapsedSections["notes"] = !collapsed }, + ) } - val displayNotes = textNotes.take(5) - items(displayNotes, key = { "note-${it.id}" }) { event -> - NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) - } - if (textNotes.size > 5) { - item(key = "notes-expand") { - ExpandableSection( - remaining = textNotes.drop(5), - ) { event -> - NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + if (!collapsed) { + val displayNotes = textNotes.take(5) + items(displayNotes, key = { "note-${it.id}" }) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } + if (textNotes.size > 5) { + item(key = "notes-expand") { + ExpandableSection( + remaining = textNotes.drop(5), + ) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } } } } @@ -145,18 +180,30 @@ fun SearchResultsList( if (people.isNotEmpty() || textNotes.isNotEmpty()) { item(key = "divider-articles") { HorizontalDivider(Modifier.padding(vertical = 4.dp)) } } + val collapsed = collapsedSections["articles"] == true stickyHeader(key = "header-articles") { - SectionHeader("Articles", articles.size, Icons.Default.Article) + SortableHeader( + title = "Articles", + count = articles.size, + icon = Icons.Default.Article, + options = SearchSortOrder.EVENT_OPTIONS, + selected = eventSortOrder, + onSelect = { state.updateEventSortOrder(it) }, + collapsed = collapsed, + onToggleCollapse = { collapsedSections["articles"] = !collapsed }, + ) } - items(articles.take(5), key = { "article-${it.id}" }) { event -> - NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) - } - if (articles.size > 5) { - item(key = "articles-expand") { - ExpandableSection( - remaining = articles.drop(5), - ) { event -> - NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + if (!collapsed) { + items(articles.take(5), key = { "article-${it.id}" }) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } + if (articles.size > 5) { + item(key = "articles-expand") { + ExpandableSection( + remaining = articles.drop(5), + ) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } } } } @@ -165,11 +212,23 @@ fun SearchResultsList( // Other section if (otherNotes.isNotEmpty()) { item(key = "divider-other") { HorizontalDivider(Modifier.padding(vertical = 4.dp)) } + val collapsed = collapsedSections["other"] == true stickyHeader(key = "header-other") { - SectionHeader("Other", otherNotes.size, Icons.Default.Forum) + SortableHeader( + title = "Other", + count = otherNotes.size, + icon = Icons.Default.Forum, + options = SearchSortOrder.EVENT_OPTIONS, + selected = eventSortOrder, + onSelect = { state.updateEventSortOrder(it) }, + collapsed = collapsed, + onToggleCollapse = { collapsedSections["other"] = !collapsed }, + ) } - items(otherNotes.take(5), key = { "other-${it.id}" }) { event -> - NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + if (!collapsed) { + items(otherNotes.take(5), key = { "other-${it.id}" }) { event -> + NotePreviewCard(event = event, onClick = { onNavigateToThread(event.id) }) + } } } @@ -179,31 +238,67 @@ fun SearchResultsList( } @Composable -private fun SectionHeader( +private fun SortableHeader( title: String, count: Int, icon: ImageVector, + options: List, + selected: SearchSortOrder, + onSelect: (SearchSortOrder) -> Unit, + collapsed: Boolean = false, + onToggleCollapse: () -> Unit = {}, ) { + val chevronRotation by animateFloatAsState(if (collapsed) -90f else 0f) + Surface( color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxWidth(), ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(vertical = 8.dp), + modifier = Modifier.clickable(onClick = onToggleCollapse).padding(vertical = 4.dp), ) { + Icon( + Icons.Default.ExpandMore, + contentDescription = if (collapsed) "Expand $title" else "Collapse $title", + modifier = Modifier.size(18.dp).rotate(chevronRotation), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) Icon( icon, contentDescription = null, - modifier = Modifier.size(18.dp), + modifier = Modifier.padding(start = 4.dp).size(18.dp), tint = MaterialTheme.colorScheme.primary, ) Text( "$title ($count)", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 8.dp), ) + Spacer(Modifier.weight(1f)) + if (!collapsed) { + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + options.forEach { option -> + FilterChip( + selected = option == selected, + onClick = { onSelect(option) }, + label = { + Text( + option.label, + style = MaterialTheme.typography.labelSmall, + ) + }, + colors = + FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primaryContainer, + selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + modifier = Modifier.height(28.dp), + ) + } + } + } } } } diff --git a/docs/brainstorms/2026-03-10-advanced-search-brainstorm.md b/docs/brainstorms/2026-03-10-advanced-search-brainstorm.md new file mode 100644 index 0000000000..609a0fdfb6 --- /dev/null +++ b/docs/brainstorms/2026-03-10-advanced-search-brainstorm.md @@ -0,0 +1,230 @@ +# Brainstorm: Advanced Search for Desktop + +**Date:** 2026-03-10 +**Status:** Draft +**Branch:** TBD (`feat/desktop-advanced-search`) + +## What We're Building + +Full-featured advanced search for Amethyst Desktop with: +1. **Twitter-style query operators** (`from:`, `kind:`, `since:`, etc.) that map to NIP-50 Filter fields +2. **Form-based UI** (expandable panel below search bar) for users who don't want to learn syntax +3. **Bidirectional sync** between text operators and form controls — editing one updates the other +4. **Extensible kind presets** — toggle groups like Notes, Articles, Media, Communities +5. **Future AI bridge** (follow-up) — natural language → structured query conversion + +**Philosophy:** Relay-first, pragmatic. Desktop users have resources; we prioritize completeness over privacy. Privacy controls available but not default friction. + +## Why This Approach + +- **No standard Nostr query language exists** — we define one that maps cleanly to `Filter` fields +- **Dual interface (text + form)** — power users get speed, casual users get discoverability +- **Current desktop search is minimal** — only kind 0 + 1, no operators, no kind filtering +- **Android has 30+ kinds** but no query language — desktop leapfrogs with both +- **AI deferred** — core query system must work standalone first; AI is a parsing layer on top + +## How Other Clients Do Search + +| Client | Approach | Operators | Notable | +|--------|----------|-----------|---------| +| **Amethyst Android** | Local cache + NIP-50, 30+ kinds | None (plain text) | Search relay list (kind 10007) | +| **Primal** | Proprietary caching server | Form UI only | Event type, time range, scope dropdowns | +| **Coracle** | NIP-50 + configurable relays | None | Also supports DVM requests (NIP-90) | +| **Damus** | NIP-50 relay search | None | User/profile focused | +| **noStrudel** | Local relay + NIP-50 | None | IndexedDB local indexing | +| **Gossip** | NIP-50 relay search | None | Desktop Rust client | +| **Noogle.lol** | NIP-90 DVM search | `from:npub` `from:me` | Pay-per-query via Lightning | + +**Key insight:** No client ships a query operator language. This is greenfield. + +## Proposed Query Schema + +Operators map directly to `Filter` fields and NIP-50 extensions: + +### Core Operators + +| Operator | Maps To | Example | Notes | +|----------|---------|---------|-------| +| `from:` | `Filter.authors` | `from:npub1abc...` | Resolve names via NIP-05/local cache | +| `kind:` | `Filter.kinds` | `kind:note` or `kind:1` | Named aliases for common kinds | +| `since:` | `Filter.since` | `since:2025-01-01` | ISO 8601 date parsing | +| `until:` | `Filter.until` | `until:2025-06-30` | ISO 8601 date parsing | +| `#` | `Filter.tags["t"]` | `#bitcoin` | Hashtag filter | +| `"exact phrase"` | Quoted in `Filter.search` | `"lightning network"` | Relay-dependent support | +| `-` | Client-side exclusion | `-spam` | Post-filter after relay results | + +### NIP-50 Extension Operators + +| Operator | NIP-50 Extension | Example | +|----------|-----------------|---------| +| `lang:` | `language:xx` | `lang:en` | +| `domain:` | `domain:xx` | `domain:nostr.com` | +| `nsfw:` | `nsfw:xx` | `nsfw:false` | + +### Kind Name Aliases + +| Alias | Kind(s) | Description | +|-------|---------|-------------| +| `note` | 1 | Short text note | +| `article` | 30023 | Long-form content | +| `repost` | 6 | Reposts | +| `reply` | 1 (with `e` tag) | Replies (client-side filter) | +| `media` | 1 (with `imeta` tag) | Notes with media | +| `channel` | 40, 41, 42 | Public channels | +| `live` | 30311 | Live activities | +| `community` | 34550 | Communities | +| `wiki` | 30818 | Wiki pages | +| `video` | 34235 | Video events | +| `classified` | 30402 | Classifieds | +| `profile` | 0 | Metadata/profiles | + +### Boolean Logic + +| Syntax | Behavior | +|--------|----------| +| `bitcoin lightning` | AND (default) | +| `bitcoin OR lightning` | OR — requires multiple relay queries | +| `-spam` | NOT — client-side exclusion | + +## UX Design + +### Search Bar (Default State) +``` +[ Search notes, people, tags... ] [Advanced v] +``` + +### Expanded Advanced Panel +``` +[ from:npub1abc kind:note since:2025-01 bitcoin ] [Advanced ^] ++------------------------------------------------------------------+ +| Content Type: [x] Notes [ ] Articles [ ] Media [ ] All | +| Author: [ npub or name... ] [+ Add] | +| Date Range: [ 2025-01-01 ] to [ today ] | +| Language: [ Any v ] | +| Hashtags: [ #bitcoin ] [+ Add] | +| Exclude: [ spam, nsfw... ] [+ Add] | +| | +| [Clear Filters] [Search] | ++------------------------------------------------------------------+ +``` + +### Bidirectional Sync +- Typing `from:npub1abc` in search bar → Author field populates in panel +- Selecting "Articles" checkbox → `kind:article` appears in search bar +- Editing either side updates the other in real-time +- Form is the "visual representation" of the query string + +### Result Display +- Results grouped by type: People, Notes, Articles, Channels +- Each result shows: content preview, author, timestamp, kind badge +- Infinite scroll with "Load more from relays" button +- Sort: Relevance (default, relay-determined) or Chronological + +## Architecture + +### Query Pipeline + +``` +User Input (text or form) + | + v +QueryParser (commons/commonMain) + |-- Tokenize operators: from:, kind:, since:, etc. + |-- Resolve names → hex pubkeys (local cache + NIP-05) + |-- Parse dates → unix timestamps + |-- Map kind aliases → kind numbers + | + v +SearchQuery (data class in commons) + |-- text: String (free text for NIP-50 search field) + |-- authors: List + |-- kinds: List + |-- since: Long? + |-- until: Long? + |-- tags: Map> + |-- excludeTerms: List + |-- language: String? + |-- nip50Extensions: Map + | + v +FilterBuilder (desktop) + |-- Convert SearchQuery → List + |-- Split by kind groups (like Android: 3 filters, ~10 kinds each) + |-- Inject NIP-50 extensions into search string + | + v +Relay Subscription + |-- Use search relay list (kind 10007) or connected relays + |-- Send REQ with filters + |-- Aggregate results + | + v +Client-side Post-filter + |-- Apply exclusions (-term) + |-- Apply "reply" detection (has e tag) + |-- Apply "media" detection (has imeta tag) + | + v +Results Display +``` + +### Module Placement + +| Component | Module | Rationale | +|-----------|--------|-----------| +| `QueryParser` | `commons/commonMain` | Reusable for Android later | +| `SearchQuery` | `commons/commonMain` | Shared data model | +| `QuerySerializer` | `commons/commonMain` | SearchQuery ↔ string conversion | +| `AdvancedSearchPanel` | `desktopApp` | Desktop-specific UI | +| `SearchScreen` (updated) | `desktopApp` | Desktop layout | +| `FilterBuilder` (updated) | `desktopApp` | Desktop filter assembly | +| Kind alias registry | `commons/commonMain` | Shared kind name mapping | + +### Search Relay Management + +- Use existing `SearchRelayListEvent` (kind 10007) from quartz +- Desktop UI to configure search relays (settings page) +- Default fallback: `relay.nostr.band`, `nostr.wine`, `relay.damus.io` +- Future: auto-discover NIP-50 capable relays via NIP-11 + +## Privacy Considerations + +| Concern | Mitigation | Default | +|---------|-----------|---------| +| Relay sees search queries | User-configurable search relay list | On (kind 10007) | +| Relay sees IP + query | VPN/Tor support (system-level) | Not enforced | +| Query history stored | No server-side history; client-side optional | Off | +| NIP-05 resolution leaks interest | Cache NIP-05 lookups locally | On | +| Author search reveals social graph | Already visible via follow lists | N/A | + +**Stance:** Relay-first, pragmatic. Desktop users accept relay visibility for better results. Advanced users can configure search relays or use Tor. + +## Key Decisions + +1. **Query language is NOT a Nostr standard** — it's a client-side UX convention that maps to `Filter` fields +2. **Bidirectional sync** between text bar and form panel — single source of truth (`SearchQuery` data class) +3. **Kind presets with extensibility** — start with core groups, users can toggle individual kinds +4. **Relay-first execution** — NIP-50 search as primary, local cache as supplement +5. **AI deferred** — follow-up feature, will parse natural language → `SearchQuery` +6. **Query parser in commons** — shared module so Android can adopt later +7. **Client-side post-filtering** for operators relays can't handle (exclusions, reply detection, media detection) + +## Resolved Questions + +1. **Name resolution** — Async + refine. Search immediately with text, resolve `from:name` in background, refine results when pubkey resolved. No blocking. +2. **OR queries** — Yes in v1. `bitcoin OR lightning` sends parallel relay subscriptions, merges results. +3. **Search history** — Recent history (last 20) stored locally. +4. **Saved searches** — Yes in v1. Pin/save queries. Future: persist as Nostr events. +5. **Result caching** — Session cache only (in-memory). Cleared on app restart. No disk persistence. + +## Open Questions + +None — all resolved. + +## Follow-up Features (Out of Scope) + +- AI natural language → query parsing (local Ollama / cloud API / NIP-90 DVM) +- Local SQLite FTS5 index for offline search +- NIP-90 DVM search integration +- Search analytics / trending topics +- Collaborative search (shared saved searches via Nostr events) diff --git a/docs/plans/2026-03-10-feat-desktop-advanced-search-plan.md b/docs/plans/2026-03-10-feat-desktop-advanced-search-plan.md new file mode 100644 index 0000000000..244213ebac --- /dev/null +++ b/docs/plans/2026-03-10-feat-desktop-advanced-search-plan.md @@ -0,0 +1,962 @@ +--- +title: "feat: Desktop Advanced Search with Query Operators and Form UI" +type: feat +status: active +date: 2026-03-10 +deepened: 2026-03-10 +origin: docs/brainstorms/2026-03-10-advanced-search-brainstorm.md +--- + +# Desktop Advanced Search + +## Enhancement Summary + +**Deepened on:** 2026-03-10 +**Agents used:** kotlin-expert, compose-expert, kotlin-coroutines, nostr-expert, desktop-expert, kmp-expert, best-practices-researcher, architecture-strategist, performance-oracle, code-simplicity-reviewer, security-sentinel + +### Key Improvements +1. **Bidirectional sync loop prevention** — `sourceOfChange` discriminator (TEXT/FORM/INIT) breaks parse→serialize→parse cycles +2. **Performance** — batch result accumulation via `channelFlow` + 100ms windows, cap OR to 3 terms (not 5), `@Immutable` SearchQuery +3. **Parser architecture** — hand-written recursive descent tokenizer + parser, error recovery via literal text degradation +4. **Module corrections** — SearchFilterFactory stays in desktopApp (needs SubscriptionConfig); SearchResultFilter and SearchHistoryStore can move to commons +5. **Compose patterns** — `FilterChip` for kind presets, `expandVertically(Alignment.Top)` + `fadeIn`, sticky section headers, shimmer loading +6. **Coroutine patterns** — `flatMapLatest` for auto-canceling old subscriptions, `merge()` for OR queries, `supervisorScope` for relay isolation +7. **Simplicity guidance** — MVP can cut OR queries, lang:/domain:, saved searches to ~350 LOC / 5 files. Full plan phases appropriately. + +### New Considerations Discovered +- NIP-50 extensions go inline in search string (`"bitcoin language:en"`), not as separate filter fields +- Pseudo-kinds (reply, media) need separate handling from real kinds — they're client-side post-filters, not relay filters +- `TextFieldValue` (not raw String) needed for cursor position stability during bidirectional sync +- Use `query.hashtags` → `Filter.tags["t"]` (more reliable than putting hashtags in search string) +- OR cap: 3 terms max (not 5) — 5 terms × 3 groups × 3 relays = 45 subs is too many + +--- + +## Overview + +Full-featured search for Amethyst Desktop: Twitter-style query operators (`from:`, `kind:`, `since:`, etc.), expandable form panel below search bar, bidirectional sync between text and form, extensible kind presets, OR queries, search history + saved searches. Relay-first via NIP-50. + +Current desktop search only handles kind 0 (people) + kind 1 (notes) with plain text. Android searches 30+ kinds. This closes that gap and adds capabilities neither platform has. + +## Problem Statement + +Desktop search (`SearchScreen.kt`) is minimal: +- Only `searchPeople()` (kind 0) wired to relay subscription +- `searchNotes()` exists in `FeedSubscription.kt` but not connected +- No kind filtering, no author filtering, no date ranges +- No query language — users can only type plain text or bech32 identifiers +- `SearchBarState` in commons only returns `User` results, no notes/channels + +Users can't find content they've seen, discover new content by topic, or filter by author/type/date. + +## Proposed Solution + +### Query Operator Language + +Client-side query language that maps to `Filter` fields. Not a Nostr standard — a UX convention. + +``` +from:npub1abc kind:note since:2025-01-01 bitcoin OR lightning -spam #nostr +``` + +| Operator | Maps To | Relay-side? | +|----------|---------|-------------| +| `from:` | `Filter.authors` | Yes | +| `kind:` | `Filter.kinds` | Yes | +| `since:` | `Filter.since` | Yes | +| `until:` | `Filter.until` | Yes | +| `#` | `Filter.tags["t"]` | Yes | +| `"exact phrase"` | Quoted in `Filter.search` | Yes (relay-dependent) | +| `lang:` | NIP-50 extension in search string | Relay-dependent | +| `domain:` | NIP-50 extension in search string | Relay-dependent | +| `-` | Client-side exclusion post-filter | No | +| `OR` | Parallel subscriptions, merged | Multiple queries | + +#### Research Insights: NIP-50 Protocol Details + +**NIP-50 extension placement:** Extensions go *inline in the search string*, not as separate filter fields. The relay parses them out: +```json +{"kinds": [1], "search": "bitcoin language:en domain:nostr.com"} +``` + +**Hashtag handling:** Use `tags = {"t": ["bitcoin"]}` in the filter (more reliable across relays) rather than putting `#bitcoin` in the search string. Hashtags in `Filter.tags` are protocol-level, not NIP-50 dependent. + +**Quoted phrase search:** Not standardized — relay-dependent. Some relays treat quotes literally, others ignore them. Degrade gracefully. + +**All filter fields AND together** within a single filter. OR requires separate subscriptions. + +### Dual UI: Text Bar + Expandable Form Panel + +``` +[ from:npub1abc kind:note bitcoin ] [Advanced v] ++----------------------------------------------------------+ +| Content: [x] Notes [ ] Articles [ ] Media [ ] All | +| Author: [ npub or name... ] [+ Add] | +| Since: [ 2025-01-01 ] Until: [ today ] | +| Tags: [ #bitcoin ] [+ Add] | +| Exclude: [ spam ] [+ Add] | +| Language:[ Any v ] | +| | +| [Clear] [Search] | ++----------------------------------------------------------+ +``` + +Bidirectional: typing `kind:article` checks "Articles"; checking "Notes" inserts `kind:note`. + +#### Research Insights: Bidirectional Sync + +**Critical: `sourceOfChange` discriminator.** Without this, parse→serialize→parse loops will occur. Track who initiated the change: + +```kotlin +enum class ChangeSource { TEXT, FORM, INIT } + +fun updateFromText(rawText: String) { + _changeSource = ChangeSource.TEXT + _query.value = QueryParser.parse(rawText) +} + +fun updateKinds(kinds: List) { + _changeSource = ChangeSource.FORM + _query.value = _query.value.copy(kinds = kinds) +} + +// In the composable, only update text field when source != TEXT +val displayText by remember { + state.query.map { query -> + if (state.changeSource != ChangeSource.TEXT) { + QuerySerializer.serialize(query) + } else { + // Keep user's raw text as-is + state.rawText + } + } +} +``` + +**Use `TextFieldValue` (not raw String)** for the text bar to preserve cursor position during form-driven updates. When form changes update the serialized text, set `TextFieldValue(text = newText, selection = TextRange(newText.length))`. + +## Technical Approach + +### Architecture + +``` +Text Bar ──parse──> SearchQuery <──serialize── Form Panel + | + FilterBuilder + | + List (split by kind groups, ~10 kinds each) + | + Relay Subscriptions (NIP-50) + | + Client-side Post-filter (exclusions, reply/media detection) + | + Results Display (grouped: People, Notes, Articles, Channels) +``` + +**Single source of truth:** `MutableStateFlow`. Both text bar and form read from it. Text bar changes → `QueryParser` → `SearchQuery`. Form changes → mutate `SearchQuery` directly. `QuerySerializer` regenerates text string. `sourceOfChange` discriminator prevents update loops. + +**Debounce strategy:** Text input debounced 300ms (existing pattern). Form toggle changes trigger immediate search (no debounce). + +#### Research Insights: Kotlin State Patterns + +**`@Immutable` on SearchQuery** — enables Compose to skip recomposition when query hasn't changed: +```kotlin +@Immutable +data class SearchQuery( + val text: String = "", + val authors: ImmutableList = persistentListOf(), + val kinds: ImmutableList = persistentListOf(), + // ... +) { + companion object { + val EMPTY = SearchQuery() + } +} +``` + +Use `kotlinx.collections.immutable` (`ImmutableList`, `ImmutableSet`, `persistentListOf()`) for all collection fields. This gives Compose structural stability guarantees. + +**Granular derived StateFlows** with `distinctUntilChanged()` to prevent unnecessary recomposition: +```kotlin +val kindsForUI: StateFlow> = _query + .map { it.kinds } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.WhileSubscribed(5000), persistentListOf()) +``` + +### Data Flow Detail + +``` +SearchQuery (SSOT) + │ + ├─ text bar reads: QuerySerializer.serialize(query) → displayed string + │ └─ on text change: QueryParser.parse(rawText) → new SearchQuery + │ └─ GUARD: only serialize→display when changeSource != TEXT + │ + ├─ form panel reads: query.kinds, query.authors, query.since, etc. + │ └─ on form change: query.copy(kinds = ...) → new SearchQuery + │ + └─ relay layer reads: SearchFilterFactory.createFilters(query) → List + └─ subscription created per filter group + └─ OR queries: parallel subscriptions via merge(), results deduped by event ID +``` + +### Implementation Phases + +#### Phase 1: Query Engine (commons/commonMain) — Foundation + +Pure Kotlin, no UI, exhaustively unit-tested. + +**Step 1.1: `SearchQuery` data class** + +```kotlin +// commons/src/commonMain/.../search/SearchQuery.kt +@Immutable +data class SearchQuery( + val text: String = "", // Free text for NIP-50 search field + val authors: ImmutableList = persistentListOf(), // Hex pubkeys + val authorNames: ImmutableList = persistentListOf(), // Unresolved names (for display) + val kinds: ImmutableList = persistentListOf(), // Empty = all searchable kinds + val since: Long? = null, // Unix timestamp + val until: Long? = null, + val hashtags: ImmutableList = persistentListOf(), // Without # prefix + val excludeTerms: ImmutableList = persistentListOf(), // Client-side exclusion + val language: String? = null, // ISO 639-1 + val domain: String? = null, // NIP-05 domain + val orTerms: ImmutableList = persistentListOf(), // Terms joined by OR +) { + val isEmpty get() = text.isBlank() && authors.isEmpty() && kinds.isEmpty() + && since == null && until == null && hashtags.isEmpty() + && orTerms.isEmpty() + + companion object { + val EMPTY = SearchQuery() + } +} +``` + +#### Research Insights: Pseudo-Kinds + +**Separate pseudo-kinds from real kinds.** `kind:reply` and `kind:media` are NOT relay filter kinds — they require client-side post-filtering: +- `kind:reply` = kind 1 events WITH `e` tag +- `kind:media` = kind 1 events WITH `imeta` tag or image URLs + +The `SearchQuery` should track these separately or the `KindRegistry` should flag them. Recommended: a `pseudoKinds: Set` field or handle in `SearchResultFilter`. + +**Step 1.2: Kind alias registry** + +```kotlin +// commons/src/commonMain/.../search/KindRegistry.kt +object KindRegistry { + // Import quartz KIND constants instead of hardcoding numbers + val aliases: Map> = mapOf( + "note" to listOf(1), + "article" to listOf(30023), + "repost" to listOf(6), + "profile" to listOf(0), + "channel" to listOf(40, 41, 42), + "live" to listOf(30311), + "community" to listOf(34550), + "wiki" to listOf(30818), + "video" to listOf(34235), + "classified" to listOf(30402), + "highlight" to listOf(9802), + "poll" to listOf(6969), + ) + + // Pseudo-kinds: client-side post-filters, not relay kinds + val pseudoKinds: Set = setOf("reply", "media") + + val presets: Map> = mapOf( + "Notes" to listOf(1), + "Articles" to listOf(30023), + "Media" to listOf(1), // post-filtered for imeta tag + "Channels" to listOf(40, 41, 42), + "Communities" to listOf(34550), + "Wiki" to listOf(30818), + ) + + fun resolve(alias: String): List? = aliases[alias.lowercase()] + fun isPseudoKind(alias: String): Boolean = alias.lowercase() in pseudoKinds + fun nameFor(kind: Int): String? = aliases.entries.find { kind in it.value }?.key +} +``` + +**Step 1.3: `QueryParser`** + +#### Research Insights: Parser Architecture + +**Hand-written recursive descent parser** (not regex, not parser generators). Two-phase: + +1. **Tokenizer** (state machine): Walks characters, emits tokens: `OperatorToken(name, value)`, `TextToken(value)`, `OrToken`, `QuotedToken(value)`, `NegationToken(value)`, `HashtagToken(value)` +2. **Parser** (recursive descent): Consumes tokens, builds `SearchQuery` + +**Key principles:** +- **Preserve raw text in tokens** for roundtrip fidelity (`serialize(parse(input))` ≈ `input`) +- **Error recovery**: malformed operators degrade to literal text, never throw +- **OR precedence**: OR binds to adjacent text terms only. Operators are always AND. + - `from:vitor bitcoin OR lightning kind:note` = `from:vitor AND kind:note AND (bitcoin OR lightning)` +- **Performance**: sub-microsecond parsing, not a concern + +```kotlin +// commons/src/commonMain/.../search/QueryParser.kt +object QueryParser { + fun parse(input: String): SearchQuery { + val tokens = tokenize(input) + return buildQuery(tokens) + } + + private fun tokenize(input: String): List { /* state machine */ } + private fun buildQuery(tokens: List): SearchQuery { /* recursive descent */ } +} + +sealed interface Token { + data class Operator(val name: String, val value: String, val raw: String) : Token + data class Text(val value: String) : Token + data object Or : Token + data class Quoted(val value: String, val raw: String) : Token + data class Negation(val term: String) : Token + data class Hashtag(val tag: String) : Token +} +``` + +Rules: +- Case-insensitive operator matching (`FROM:` = `from:`) +- `from:` → if bech32 npub, decode to hex and add to `authors`; else add to `authorNames` (async resolution) +- `kind:` → resolve via `KindRegistry.resolve()` or parse as int. Flag pseudo-kinds separately. +- `since:` / `until:` → parse ISO 8601 (`2025-01-01`, `2025-01`, `2025`) to unix timestamp +- `#tag` → add to `hashtags` +- `"quoted phrase"` → keep in `text` as quoted +- `-term` → add to `excludeTerms`, strip from relay search string +- `OR` → split adjacent free text terms. `bitcoin OR lightning` → `orTerms = ["bitcoin", "lightning"]` +- Multiple `from:` → AND (multiple authors) +- Multiple `kind:` → union (combined kinds) +- Incomplete operators (`from:` with no value) → treat as literal text + +**Step 1.4: `QuerySerializer`** + +```kotlin +// commons/src/commonMain/.../search/QuerySerializer.kt +object QuerySerializer { + fun serialize(query: SearchQuery): String { ... } +} +``` + +Regenerates the canonical text representation from `SearchQuery`. Used to update text bar when form changes. Ordering: operators first (`from:`, `kind:`, `since:`, `until:`, `lang:`, `domain:`), then hashtags, then free text / OR terms, then exclusions. + +**Step 1.5: Unit tests** + +```kotlin +// commons/src/commonTest/.../search/QueryParserTest.kt +// commons/src/commonTest/.../search/QuerySerializerTest.kt +// commons/src/commonTest/.../search/KindRegistryTest.kt +``` + +Test matrix: +- Single operator of each type +- Combined operators +- OR with operators +- Malformed/incomplete (`from:`, `kind:invalid`, `since:not-a-date`) +- Special characters, emoji, unicode in free text +- Roundtrip: `serialize(parse(input)) == normalized(input)` +- Multiple `from:` authors +- Multiple `kind:` (union) +- Quoted phrases +- Exclusion terms +- Pseudo-kind detection (`kind:reply`, `kind:media`) +- Edge: empty string, whitespace only, very long query +- OR precedence: `from:x a OR b kind:note` → operators AND, text OR + +**Consider property-based testing** with Kotest for roundtrip fidelity. + +#### Phase 2: Filter Factory + Relay Integration (desktopApp) + +**Step 2.1: `SearchFilterFactory`** + +```kotlin +// desktopApp/src/jvmMain/.../subscriptions/SearchFilterFactory.kt +object SearchFilterFactory { + fun createFilters(query: SearchQuery): List { ... } +} +``` + +- If `query.kinds` specified → use those kinds directly +- If `query.kinds` empty → use default searchable kinds (align with Android's 3 groups) +- Split kinds into groups of ~10 (relay `max_filters` limit safety) +- Build NIP-50 search string: `query.text` + inline NIP-50 extensions (`language:en`, `domain:x`) +- Strip exclusion terms from search string (don't send `-spam` to relay) +- `query.authors` → `Filter.authors` (only resolved hex keys) +- `query.since` / `query.until` → `Filter.since` / `Filter.until` +- `query.hashtags` → `Filter.tags["t"]` (not in search string — more reliable) +- OR queries: return separate filter lists per OR term + +#### Research Insights: Module Placement + +**SearchFilterFactory stays in desktopApp** — it depends on `SubscriptionConfig` and relay topology, which are desktop-specific. Correct as planned. + +**SearchResultFilter can move to commons/commonMain** — pure Kotlin, no platform dependencies. Android can reuse it later. + +**SearchHistoryStore can move to commons as expect/actual** — follows `SecureKeyStorage` pattern. `expect class SearchHistoryStore`, with `actual` implementations using `java.util.prefs.Preferences` on desktop and SharedPreferences/DataStore on Android. + +**Step 2.2: Default searchable kind groups** + +Port from Android's `SearchPostsByText.kt` to desktop. Reference the same kinds: + +```kotlin +// Group 1: TextNote, LongText, Badge, PeopleList, BookmarkList, AudioHeader, AudioTrack, PinList, PollNote, ChannelCreate +// Group 2: ChannelMetadata, Classifieds, Community, EmojiPack, Highlight, LiveActivities, PublicMessage, NNS, Wiki, Comment +// Group 3: InteractiveStory (2 kinds), FollowList, NipText, Poll, PollResponse +``` + +Kind group splitting is relay-imposed (`max_filters` limits), not protocol. Use the quartz KIND constants, don't hardcode numbers. + +**Step 2.3: Search subscription factory** + +```kotlin +// desktopApp/src/jvmMain/.../subscriptions/FeedSubscription.kt (extend) +fun createAdvancedSearchSubscription( + relays: Set, + query: SearchQuery, + onEvent: ..., + onEose: ..., +): List +``` + +#### Research Insights: Subscription Management + +**Use `flatMapLatest`** on debouncedQuery to auto-cancel old subscriptions when query changes: +```kotlin +val results: Flow> = debouncedQuery + .flatMapLatest { query -> + if (query.isEmpty) flowOf(emptyList()) + else channelFlow { + supervisorScope { + val filters = SearchFilterFactory.createFilters(query) + // Launch independent subscription per filter group + filters.forEach { filter -> + launch { subscribeAndEmit(filter, relays) } + } + } + } + } +``` + +**`supervisorScope`** for independent relay subscription failure isolation — one relay failure doesn't cancel others. + +**`merge()` (not `combine()`)** for OR query result flows — emit results as they arrive from any term. + +**Batch filters per OR term** in a single REQ (not per kind group), reducing subscription count: +- 3 OR terms × 1 batched REQ × 3 relays = 9 subscriptions (vs 45 if unbatched) + +**Cap: max 3 OR terms** (not 5) — subscription fan-out gets expensive. + +**Step 2.4: Client-side post-filter** + +```kotlin +// commons/src/commonMain/.../search/SearchResultFilter.kt +object SearchResultFilter { + fun filter(events: List, query: SearchQuery): List +} +``` + +- `-term` exclusion: check `event.content` doesn't contain term (case-insensitive) +- `kind:reply` detection: kind 1 with `e` tag +- `kind:media` detection: kind 1 with `imeta` tag or URL patterns +- Deduplication by event ID (for OR query merges) + +#### Research Insights: Performance + +**Batch result accumulation** — current Amethyst pattern of `_results.value = _results.value + item` is O(n^2). Use channel-based batching: + +```kotlin +channelFlow { + val batch = mutableSetOf() // Set for O(1) dedup + var lastEmit = 0L + + onEvent = { event -> + batch.add(event) + val now = System.currentTimeMillis() + if (now - lastEmit > 100) { // 100ms batch window + send(batch.toList()) + lastEmit = now + } + } +} +``` + +**Apply post-filter at batch emission time**, not per-event. + +**Result ordering:** dedup by event ID, sort by `createdAt` descending (match Amethyst Android behavior). + +#### Phase 3: Advanced Search State (commons/commonMain) + +**Step 3.1: `AdvancedSearchBarState`** + +New state holder that extends/replaces `SearchBarState`. Manages the `SearchQuery` as SSOT. + +```kotlin +// commons/src/commonMain/.../viewmodels/AdvancedSearchBarState.kt +class AdvancedSearchBarState( + private val cache: ICacheProvider, + private val scope: CoroutineScope, +) { + private val _query = MutableStateFlow(SearchQuery.EMPTY) + val query: StateFlow = _query.asStateFlow() + + // Track who initiated the change (prevents sync loops) + private var _changeSource: ChangeSource = ChangeSource.INIT + val changeSource get() = _changeSource + + // Raw text from user typing (preserved when source=TEXT) + private val _rawText = MutableStateFlow("") + val rawText: StateFlow = _rawText.asStateFlow() + + // Derived: text representation for the search bar + val displayText: StateFlow = combine(_query, _rawText) { query, raw -> + if (_changeSource == ChangeSource.TEXT) raw + else QuerySerializer.serialize(query) + }.stateIn(scope, SharingStarted.Eagerly, "") + + // For relay subscriptions to observe (300ms debounce) + val debouncedQuery: StateFlow = _query + .debounce(300) + .stateIn(scope, SharingStarted.Eagerly, SearchQuery.EMPTY) + + // Results + val peopleResults: StateFlow> + val noteResults: StateFlow> + val isSearching: StateFlow + + // Text bar input (parses into SearchQuery) + fun updateFromText(rawText: String) { + _changeSource = ChangeSource.TEXT + _rawText.value = rawText + _query.value = QueryParser.parse(rawText) + } + + // Form panel input (mutates SearchQuery directly) + fun updateKinds(kinds: List) { + _changeSource = ChangeSource.FORM + _query.value = _query.value.copy(kinds = kinds.toImmutableList()) + } + + fun addAuthor(hexOrName: String) { ... } + fun removeAuthor(hex: String) { ... } + fun updateDateRange(since: Long?, until: Long?) { ... } + fun addHashtag(tag: String) { ... } + fun removeHashtag(tag: String) { ... } + fun addExcludeTerm(term: String) { ... } + fun updateLanguage(lang: String?) { ... } + + // Name resolution (async) + fun resolveAuthorName(name: String, onResolved: (String) -> Unit) + + // History + fun addToHistory(query: SearchQuery) + fun getHistory(): List + fun saveSearch(query: SearchQuery, label: String) + fun getSavedSearches(): List + fun deleteSavedSearch(id: String) +} + +enum class ChangeSource { TEXT, FORM, INIT } +``` + +**Step 3.2: Name resolution** + +- Check local cache first: `cache.findUsersStartingWith(name, 5)` +- If multiple matches → expose as `authorSuggestions: StateFlow>` for autocomplete dropdown +- If single match → auto-resolve to hex key +- If no local match → keep as `authorNames` (display in form as "unresolved: vitor") +- Keep name resolution **out of QueryParser** — parser returns raw strings, platform layer resolves +- No NIP-05 resolution in v1. Follow-up. + +#### Phase 4: Desktop UI (desktopApp) + +**Step 4.1: Rewrite `SearchScreen.kt`** + +```kotlin +// desktopApp/src/jvmMain/.../ui/SearchScreen.kt +@Composable +fun SearchScreen( + localCache: DesktopLocalCache, + relayManager: DesktopRelayConnectionManager, + ... +) { + val state = remember { AdvancedSearchBarState(localCache, scope) } + val query by state.query.collectAsState() + val displayText by state.displayText.collectAsState() + var panelExpanded by remember { mutableStateOf(false) } + + Column { + // Search bar row + Row { + OutlinedTextField( + value = TextFieldValue( + text = displayText, + selection = TextRange(displayText.length), + ), + onValueChange = { state.updateFromText(it.text) }, + placeholder = { Text("Search notes, people, tags... or use operators") }, + ... + ) + TextButton(onClick = { panelExpanded = !panelExpanded }) { + Text(if (panelExpanded) "Advanced ^" else "Advanced v") + } + } + + // Expandable advanced panel + AnimatedVisibility( + visible = panelExpanded, + enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(), + exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(), + ) { + AdvancedSearchPanel( + query = query, + onKindsChanged = { state.updateKinds(it) }, + onAuthorAdded = { state.addAuthor(it) }, + onAuthorRemoved = { state.removeAuthor(it) }, + onDateRangeChanged = { since, until -> state.updateDateRange(since, until) }, + ... + ) + } + + // Results + SearchResultsList(state = state, ...) + } +} +``` + +#### Research Insights: Compose UI Patterns + +**Panel animation:** `expandVertically(expandFrom = Alignment.Top)` + `fadeIn()` — panel slides down from search bar, feels natural. + +**Kind preset chips:** Use `FilterChip` (not ElevatedFilterChip or AssistChip): +```kotlin +KindRegistry.presets.forEach { (name, kinds) -> + FilterChip( + selected = query.kinds.containsAll(kinds), + onClick = { onKindsChanged(toggleKinds(query.kinds, kinds)) }, + label = { Text(name) }, + ) +} +``` + +**Author autocomplete:** `DropdownMenu` (not `Popup`) — handles dismissal, positioning, focus correctly. + +**Date input:** Text fields with `YYYY-MM-DD` format (no native date picker on desktop). Validate on blur. + +**Keyboard events:** `onPreviewKeyEvent` for Escape (before children), `onKeyEvent` for `/` (after children). + +**Step 4.2: `AdvancedSearchPanel` composable** + +```kotlin +// desktopApp/src/jvmMain/.../ui/search/AdvancedSearchPanel.kt +@Composable +fun AdvancedSearchPanel( + query: SearchQuery, + onKindsChanged: (List) -> Unit, + onAuthorAdded: (String) -> Unit, + ... +) +``` + +Components: +- **Content type row**: `FilterChip` per preset from `KindRegistry.presets`. Checked state derived from `query.kinds`. +- **Author field**: `OutlinedTextField` + `DropdownMenu` autocomplete dropdown (from `authorSuggestions`). Shows chips for added authors. +- **Date range**: Two date text fields (`yyyy-MM-dd` format). Validate on blur. +- **Hashtags**: Chip group with add button. +- **Exclude terms**: Chip group with add button. +- **Language dropdown**: `DropdownMenu` with common ISO 639-1 codes. +- **Clear / Search buttons**: Clear resets `SearchQuery.EMPTY`. Search is implicit (debounced). +- **Tooltips**: `TooltipBox` + `PlainTooltip` for operator hint text on hover. + +**Step 4.3: `SearchResultsList` composable** + +```kotlin +// desktopApp/src/jvmMain/.../ui/search/SearchResultsList.kt +@Composable +fun SearchResultsList(state: AdvancedSearchBarState, ...) +``` + +#### Research Insights: Results Display + +- Single `LazyColumn` with **sticky section headers**: `stickyHeader { Surface(color = background) { ... } }` +- Sections: **People** (kind 0), **Notes** (kind 1), **Articles** (kind 30023), **Other** (everything else) +- Each section shows top 5 results with "Show all N" expand link +- Note results: content preview (first 200 chars), author name, timestamp, kind badge +- Progressive loading: results stream in as relay responds, sections update live +- **Shimmer loading**: Custom shimmer via `Brush.linearGradient` + `InfiniteTransition` (reusable, put in commons) +- Empty state: "No results found. Try broader terms or fewer filters." +- **Stable keys** for LazyColumn items: `key = { "section-${event.id}" }` to prevent recomposition flicker + +**Step 4.4: Relay subscription wiring** + +In `SearchScreen.kt`, use `rememberSubscription()` with the debounced query: + +```kotlin +val debouncedQuery by state.debouncedQuery.collectAsState() +val configuredRelays by remember { + relayManager.relayStatuses + .map { it.keys } + .distinctUntilChanged() // Prevent churn (FeedScreen pattern) +}.collectAsState(emptySet()) + +// Create subscriptions from query +val filters = remember(debouncedQuery) { SearchFilterFactory.createFilters(debouncedQuery) } +// ... wire up rememberSubscription per filter group +``` + +#### Research Insights: Desktop-Specific Patterns + +**Keyboard shortcuts:** +- `Ctrl+K` or `/` → focus search bar. Use `Window.onKeyEvent` for `/` (after children process), `onPreviewKeyEvent` for Escape. +- `Escape` → close advanced panel / clear search +- `Enter` → execute search immediately (skip debounce) +- Register `Ctrl+K` in `MenuBar { Item("Search", KeyShortcut(Key.K, ctrl = true)) { focusSearch() } }` + +**Clipboard:** Support pasting npub/note/nevent directly into search bar — already handled by `QueryParser` treating bech32 as `from:` equivalent. + +#### Phase 5: Search History + Saved Searches + +**Step 5.1: Local persistence** + +```kotlin +// desktopApp/src/jvmMain/.../storage/SearchHistoryStore.kt +class SearchHistoryStore(private val appDataDir: Path) { + private val historyFile = appDataDir / "search_history.json" + private val savedFile = appDataDir / "saved_searches.json" + + // In-memory cache, async persist on Dispatchers.IO + private var historyCache: MutableList = mutableListOf() + + fun addToHistory(query: SearchQuery) // Dedup by serialized text, max 20 entries + fun getHistory(): List + fun clearHistory() + + fun saveSearch(query: SearchQuery, label: String) + fun getSavedSearches(): List + fun deleteSavedSearch(id: String) +} + +data class SavedSearch( + val id: String, // UUID + val label: String, + val query: SearchQuery, + val createdAt: Long, +) +``` + +JSON serialization via kotlinx.serialization (already in project). + +**Platform data dirs:** macOS `~/Library/Application Support/Amethyst/`, Linux `~/.config/amethyst/`, Windows `%APPDATA%\Amethyst\`. Use existing `DesktopPreferences.kt` pattern or `java.util.prefs.Preferences`. + +**Step 5.2: History UI** + +When search bar is empty → show recent history + saved searches below the bar. +- History items: click to load query into search bar +- Saved searches: click to load, X to delete +- "Clear history" button at bottom + +#### Phase 6: Integration + Polish + +**Step 6.1: Search hints update** + +Update empty state hints to show operator examples: +``` +from:npub1... Filter by author +kind:article Long-form content +since:2025-01 After January 2025 +#bitcoin Hashtag search +"exact phrase" Exact match +bitcoin OR nostr Either term +``` + +**Step 6.2: Keyboard shortcuts** + +- `Ctrl+K` or `/` → focus search bar (desktop convention) +- `Escape` → close advanced panel / clear search +- `Enter` → execute search immediately (skip debounce) + +**Step 6.3: Search relay configuration** + +- Desktop settings page: list of search relays (editable) +- Default: `relay.nostr.band`, `nostr.wine`, `relay.damus.io` (curated, don't auto-probe NIP-11) +- Future: read from kind 10007 `SearchRelayListEvent` + +## System-Wide Impact + +### Interaction Graph + +1. User types in search bar → `AdvancedSearchBarState.updateFromText()` → `QueryParser.parse()` → `_query` updates +2. `_query` change → `displayText` recomputes (serialized, guarded by `changeSource`) → text bar updates +3. `_query` change → `debouncedQuery` emits after 300ms → `flatMapLatest` cancels old subscriptions → new relay subscriptions created +4. Subscription creation → `relayManager.subscribe()` → relay receives REQ +5. Relay responds → `onEvent` callback → events batched (100ms windows) → stored in cache + state +6. Post-filter applied at batch emission → results displayed in `SearchResultsList` + +### Error Propagation + +- Relay timeout → `onEose` fires → `isSearching` set to false → "No results" shown +- Name resolution failure → name stays in `authorNames` as unresolved → user sees "unresolved: vitor" chip +- Parse error → malformed operators treated as literal text → no crash, graceful degradation +- Non-NIP-50 relay → relay ignores `search` field, returns nothing useful → handled by showing results from other relays +- Individual relay failure → `supervisorScope` isolates failure → other relays continue + +### State Lifecycle Risks + +- **Subscription churn**: Mitigated by `distinctUntilChanged()` on relay statuses (proven pattern from FeedScreen) +- **Bidirectional update loop**: Prevented by `sourceOfChange` discriminator — TEXT changes don't trigger re-serialization +- **Stale results**: Session-only cache cleared on restart. `flatMapLatest` clears previous subscription results on new query. +- **Memory pressure from result batching**: Bounded by LRU cache (500 entries) and batch window (100ms) + +### API Surface Parity + +- `SearchBarState` in commons is used by both Android and Desktop today. `AdvancedSearchBarState` extends this pattern but is new. +- `QueryParser`, `SearchQuery`, `KindRegistry`, `SearchResultFilter` placed in commons so Android can adopt later. +- Desktop `FilterBuilders` gets new `searchAdvanced()` methods but existing methods unchanged. + +## Acceptance Criteria + +### Functional + +- [x] Query operators parse correctly: `from:`, `kind:`, `since:`, `until:`, `#tag`, `"phrase"`, `-exclude`, `OR`, `lang:`, `domain:` +- [x] Kind aliases resolve: `kind:note` → kind 1, `kind:article` → kind 30023, etc. +- [x] Pseudo-kinds handled: `kind:reply` and `kind:media` flagged for client-side post-filtering +- [x] Advanced panel expands/collapses below search bar with `expandVertically` + `fadeIn` animation +- [x] Bidirectional sync: text changes update form, form changes update text, no loops (sourceOfChange guard) +- [x] Default search (no kind filter) queries 30+ kinds across 3 filter groups (Android parity) +- [x] OR queries (`bitcoin OR lightning`) send parallel subscriptions, merge + dedup results (max 3 terms) +- [x] Results grouped by type: People, Notes, Articles, Other with sticky section headers +- [x] Note results show content preview, author, timestamp, kind badge +- [x] Search history persists last 20 queries locally +- [x] Saved searches persist across sessions +- [x] Exclusion terms (`-spam`) filtered client-side, not sent to relay +- [x] Empty search bar shows history + saved searches + operator hints +- [x] `Escape` closes panel / clears search (Ctrl+K deferred — needs window-level handler) + +### Non-Functional + +- [x] Search debounce: 300ms for text, immediate for form toggles +- [x] Max 3 OR terms, 10 authors per query +- [x] Relay subscription churn prevented via `distinctUntilChanged()` +- [x] Result accumulation uses set-based dedup +- [x] `@Immutable` SearchQuery with `ImmutableList` fields +- [x] Session cache cleared on restart +- [x] All query parsing logic unit-tested in commons (roundtrip, edge cases, malformed input) + +### Quality Gates + +- [x] `QueryParser` + `QuerySerializer` roundtrip tests pass +- [x] `KindRegistry` tests for all aliases + pseudo-kinds +- [x] `SearchFilterFactory` compiles + filter generation correct +- [x] `SearchResultFilter` handles exclusion, reply detection, media detection +- [x] Desktop search screen renders results for all kind types +- [x] `spotlessApply` passes + +## Dependencies & Prerequisites + +| Dependency | Status | Notes | +|-----------|--------|-------| +| `Filter.search` field | Exists | `quartz/.../Filter.kt` | +| `SearchRelayListEvent` | Exists | `quartz/.../SearchRelayListEvent.kt` (kind 10007) | +| `SearchBarState` | Exists | `commons/.../SearchBarState.kt` — will be extended | +| `SearchParser` | Exists | `commons/.../SearchParser.kt` — bech32 parsing, kept as-is | +| `FilterBuilders` | Exists | `desktopApp/.../FilterBuilders.kt` — extended | +| `rememberSubscription()` | Exists | `desktopApp/.../SubscriptionUtils.kt` | +| `DesktopLocalCache` | Exists | Needs `findNotesStartingWith()` for local note search | +| kotlinx.serialization | In project | For search history JSON persistence | +| kotlinx.collections.immutable | **Add** | For `ImmutableList`/`persistentListOf()` in SearchQuery | + +## Risk Analysis & Mitigation + +| Risk | Likelihood | Impact | Mitigation | +|------|-----------|--------|------------| +| Bidirectional sync loops | Medium | High | `sourceOfChange` discriminator, TEXT changes preserve raw text | +| Relay subscription explosion (OR + many relays) | Medium | Medium | Cap: 3 OR terms, batch filters per term. Total max ~27 subs | +| NIP-50 relay variability | High | Medium | Graceful degradation — show whatever relays return | +| Name resolution UX confusion | Medium | Medium | Show "unresolved" indicator, autocomplete dropdown | +| O(n^2) result accumulation | Medium | Medium | Batch + set-based dedup via channelFlow | +| Large result sets from broad queries | High | Low | Client-side pagination, "Show more" per section | +| Android `SearchBarState` compatibility | Low | Medium | New `AdvancedSearchBarState`, old class untouched | + +## Simplicity Guidance (MVP Scoping) + +The full plan is comprehensive. If time-constrained, a minimal viable version can ship with: + +**MVP (Phase 1+2+4 subset, ~350 LOC, 5 files):** +- `SearchQuery` data class (no `@Immutable` yet, plain lists) +- `QueryParser` with 5 operators: `from:`, `kind:`, `since:`, `until:`, `#tag` +- `SearchFilterFactory` for filter generation +- Rewritten `SearchScreen.kt` with form panel (no bidirectional sync — form→text only) +- Unit tests for parser + +**Cut for MVP:** +- OR queries, `lang:`, `domain:`, `-exclude` +- `QuerySerializer` (not needed without bidirectional sync) +- Saved searches (history only) +- Shimmer loading states +- Keyboard shortcuts beyond Enter/Escape + +**Add incrementally:** OR queries → bidirectional sync → saved searches → keyboard shortcuts → NIP-50 extensions + +## File Matrix + +| File | Status | Module | Action | +|------|--------|--------|--------| +| `SearchQuery.kt` | New | commons/commonMain | Create data class with `@Immutable` | +| `QueryParser.kt` | New | commons/commonMain | Create recursive descent parser | +| `QuerySerializer.kt` | New | commons/commonMain | Create serializer | +| `KindRegistry.kt` | New | commons/commonMain | Create kind alias registry | +| `AdvancedSearchBarState.kt` | New | commons/commonMain | Create state holder with `sourceOfChange` | +| `SearchResultFilter.kt` | New | commons/commonMain | Create post-filter (reusable) | +| `QueryParserTest.kt` | New | commons/commonTest | Create tests | +| `QuerySerializerTest.kt` | New | commons/commonTest | Create tests | +| `KindRegistryTest.kt` | New | commons/commonTest | Create tests | +| `SearchFilterFactory.kt` | New | desktopApp | Create filter factory | +| `AdvancedSearchPanel.kt` | New | desktopApp | Create form panel composable | +| `SearchResultsList.kt` | New | desktopApp | Create results list composable | +| `SearchHistoryStore.kt` | New | desktopApp | Create persistence | +| `SearchScreen.kt` | Rewrite | desktopApp | Integrate advanced search | +| `FeedSubscription.kt` | Extend | desktopApp | Add `createAdvancedSearchSubscription()` | +| `FilterBuilders.kt` | Extend | desktopApp | Add search filter methods | +| `SearchBarState.kt` | Keep | commons/commonMain | Untouched (backward compat) | +| `SearchParser.kt` | Keep | commons/commonMain | Untouched (bech32 parsing still used) | + +## Future Considerations + +- **AI natural language → query** (deferred) — parse "notes about bitcoin from Jack since January" to operators +- **Local SQLite FTS5 index** — offline search for desktop +- **NIP-90 DVM search** — pay-per-query via Lightning +- **Search relay auto-discovery** — NIP-11 `supported_nips` check for NIP-50 +- **NIP-05 name resolution** — async resolve `from:vitor@nostr.com` +- **Saved searches as Nostr events** — portable across devices +- **Search analytics** — trending topics, popular queries + +## Sources & References + +### Origin + +- **Brainstorm document:** [docs/brainstorms/2026-03-10-advanced-search-brainstorm.md](docs/brainstorms/2026-03-10-advanced-search-brainstorm.md) — Key decisions: relay-first approach, Twitter-style operators + form UI, extensible kind presets, AI deferred, session-only caching + +### Internal References + +- Current search screen: `desktopApp/.../ui/SearchScreen.kt` +- Search state: `commons/.../viewmodels/SearchBarState.kt` +- Bech32 parser: `commons/.../search/SearchParser.kt` +- Filter class: `quartz/.../nip01Core/relay/filters/Filter.kt` +- Android kind groups: `amethyst/.../searchCommand/subassemblies/SearchPostsByText.kt` +- FilterBuilders: `desktopApp/.../subscriptions/FilterBuilders.kt` +- Feed subscriptions: `desktopApp/.../subscriptions/FeedSubscription.kt` +- Relay subscription utils: `desktopApp/.../subscriptions/SubscriptionUtils.kt` +- Relay churn fix: `desktopApp/.../ui/FeedScreen.kt:163-167` (`distinctUntilChanged()` pattern) + +### External References + +- [NIP-50 Search](https://nips.nostr.com/50) +- [NIP-50 extensions](https://github.com/nostr-protocol/nips/blob/master/50.md): `language:`, `domain:`, `sentiment:`, `nsfw:`, `include:spam` +- [kotlinx.collections.immutable](https://github.com/Kotlin/kotlinx.collections.immutable) — `ImmutableList`, `persistentListOf()` + +## Unanswered Questions + +None — all resolved in brainstorm. Implementation details clarified by research agents. From 327bb1efc855a38b85d5d583e97e3c01224733f6 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Thu, 12 Mar 2026 13:32:05 +0200 Subject: [PATCH 017/172] fix(nav): preserve screen state across overlay navigation RootContent now stays composed when overlays are shown, preventing search results and other state from being destroyed on navigation. Overlay wrapped in opaque Surface to cover root content visually. Co-Authored-By: Claude Opus 4.6 --- .../desktop/ui/deck/DeckColumnContainer.kt | 67 ++++++++++--------- .../desktop/ui/deck/SinglePaneLayout.kt | 66 +++++++++--------- 2 files changed, 73 insertions(+), 60 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckColumnContainer.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckColumnContainer.kt index ce90b7ef6c..b59abab0fe 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckColumnContainer.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/DeckColumnContainer.kt @@ -27,6 +27,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -120,37 +122,42 @@ fun DeckColumnContainer( Box( modifier = Modifier.fillMaxSize().padding(12.dp), ) { + // Always keep RootContent composed so state (e.g. search results) survives navigation + RootContent( + columnType = column.type, + relayManager = relayManager, + localCache = localCache, + accountManager = accountManager, + account = account, + nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, + appScope = appScope, + onShowComposeDialog = onShowComposeDialog, + onShowReplyDialog = onShowReplyDialog, + onZapFeedback = onZapFeedback, + onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) }, + onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) }, + ) if (currentOverlay != null) { - OverlayContent( - screen = currentOverlay, - relayManager = relayManager, - localCache = localCache, - account = account, - nwcConnection = nwcConnection, - subscriptionsCoordinator = subscriptionsCoordinator, - onShowComposeDialog = onShowComposeDialog, - onShowReplyDialog = onShowReplyDialog, - onZapFeedback = onZapFeedback, - onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) }, - onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) }, - onBack = { navState.pop() }, - ) - } else { - RootContent( - columnType = column.type, - relayManager = relayManager, - localCache = localCache, - accountManager = accountManager, - account = account, - nwcConnection = nwcConnection, - subscriptionsCoordinator = subscriptionsCoordinator, - appScope = appScope, - onShowComposeDialog = onShowComposeDialog, - onShowReplyDialog = onShowReplyDialog, - onZapFeedback = onZapFeedback, - onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) }, - onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) }, - ) + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.fillMaxSize(), + ) { + OverlayContent( + screen = currentOverlay, + relayManager = relayManager, + localCache = localCache, + account = account, + nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, + onShowComposeDialog = onShowComposeDialog, + onShowReplyDialog = onShowReplyDialog, + onZapFeedback = onZapFeedback, + onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) }, + onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) }, + onBack = { navState.pop() }, + ) + } } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt index 66e11d8d0d..a00dc029a8 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/deck/SinglePaneLayout.kt @@ -43,6 +43,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable @@ -152,37 +153,42 @@ fun SinglePaneLayout( Box( modifier = Modifier.fillMaxSize().padding(12.dp), ) { + // Always keep RootContent composed so state (e.g. search results) survives navigation + RootContent( + columnType = currentColumnType, + relayManager = relayManager, + localCache = localCache, + accountManager = accountManager, + account = account, + nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, + appScope = appScope, + onShowComposeDialog = onShowComposeDialog, + onShowReplyDialog = onShowReplyDialog, + onZapFeedback = onZapFeedback, + onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) }, + onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) }, + ) if (currentOverlay != null) { - OverlayContent( - screen = currentOverlay, - relayManager = relayManager, - localCache = localCache, - account = account, - nwcConnection = nwcConnection, - subscriptionsCoordinator = subscriptionsCoordinator, - onShowComposeDialog = onShowComposeDialog, - onShowReplyDialog = onShowReplyDialog, - onZapFeedback = onZapFeedback, - onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) }, - onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) }, - onBack = { navState.pop() }, - ) - } else { - RootContent( - columnType = currentColumnType, - relayManager = relayManager, - localCache = localCache, - accountManager = accountManager, - account = account, - nwcConnection = nwcConnection, - subscriptionsCoordinator = subscriptionsCoordinator, - appScope = appScope, - onShowComposeDialog = onShowComposeDialog, - onShowReplyDialog = onShowReplyDialog, - onZapFeedback = onZapFeedback, - onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) }, - onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) }, - ) + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.fillMaxSize(), + ) { + OverlayContent( + screen = currentOverlay, + relayManager = relayManager, + localCache = localCache, + account = account, + nwcConnection = nwcConnection, + subscriptionsCoordinator = subscriptionsCoordinator, + onShowComposeDialog = onShowComposeDialog, + onShowReplyDialog = onShowReplyDialog, + onZapFeedback = onZapFeedback, + onNavigateToProfile = { navState.push(DesktopScreen.UserProfile(it)) }, + onNavigateToThread = { navState.push(DesktopScreen.Thread(it)) }, + onBack = { navState.pop() }, + ) + } } } } From 8f02ea0cf6d18f747f88c76b1592ab2a39ba7368 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 15:13:32 +0100 Subject: [PATCH 018/172] Foundations(iOS Sourceset): Provide implementation for LibSodiumInstance.ios.kt, using the native library(shipped in project). Generate cinterop definition file with custom Gradle task. Defines bindings for use in actual code(may be streamlined later). Adds NIP44 tests and makes sure they pass. --- quartz/build.gradle.kts | 74 ++ .../quartz/utils/LibSodiumInstance.ios.kt | 74 +- .../quartz/nip44Encryption/Nip44v1Test.kt | 91 +++ .../quartz/nip44Encryption/Nip44v2Test.kt | 182 +++++ .../nip44Encryption/TestPackageClasses.kt | 103 +++ .../src/iosTest/resources/nip44.vectors.json | 664 ++++++++++++++++++ quartz/src/nativeInterop/libsodium/LICENSE | 18 + .../libsodium/include/module.modulemap | 4 + .../nativeInterop/libsodium/include/sodium.h | 81 +++ .../libsodium/include/sodium/core.h | 28 + .../include/sodium/crypto_aead_aegis128l.h | 92 +++ .../include/sodium/crypto_aead_aegis256.h | 92 +++ .../include/sodium/crypto_aead_aes256gcm.h | 179 +++++ .../sodium/crypto_aead_chacha20poly1305.h | 180 +++++ .../sodium/crypto_aead_xchacha20poly1305.h | 100 +++ .../libsodium/include/sodium/crypto_auth.h | 46 ++ .../include/sodium/crypto_auth_hmacsha256.h | 70 ++ .../include/sodium/crypto_auth_hmacsha512.h | 68 ++ .../sodium/crypto_auth_hmacsha512256.h | 65 ++ .../libsodium/include/sodium/crypto_box.h | 177 +++++ .../crypto_box_curve25519xchacha20poly1305.h | 164 +++++ .../crypto_box_curve25519xsalsa20poly1305.h | 112 +++ .../include/sodium/crypto_core_ed25519.h | 104 +++ .../include/sodium/crypto_core_hchacha20.h | 36 + .../include/sodium/crypto_core_hsalsa20.h | 36 + .../include/sodium/crypto_core_keccak1600.h | 66 ++ .../include/sodium/crypto_core_ristretto255.h | 100 +++ .../include/sodium/crypto_core_salsa20.h | 36 + .../include/sodium/crypto_core_salsa2012.h | 36 + .../include/sodium/crypto_core_salsa208.h | 40 ++ .../include/sodium/crypto_generichash.h | 84 +++ .../sodium/crypto_generichash_blake2b.h | 122 ++++ .../libsodium/include/sodium/crypto_hash.h | 40 ++ .../include/sodium/crypto_hash_sha256.h | 60 ++ .../include/sodium/crypto_hash_sha512.h | 60 ++ .../libsodium/include/sodium/crypto_ipcrypt.h | 132 ++++ .../libsodium/include/sodium/crypto_kdf.h | 53 ++ .../include/sodium/crypto_kdf_blake2b.h | 44 ++ .../include/sodium/crypto_kdf_hkdf_sha256.h | 74 ++ .../include/sodium/crypto_kdf_hkdf_sha512.h | 75 ++ .../libsodium/include/sodium/crypto_kx.h | 66 ++ .../include/sodium/crypto_onetimeauth.h | 65 ++ .../sodium/crypto_onetimeauth_poly1305.h | 72 ++ .../libsodium/include/sodium/crypto_pwhash.h | 147 ++++ .../include/sodium/crypto_pwhash_argon2i.h | 122 ++++ .../include/sodium/crypto_pwhash_argon2id.h | 122 ++++ .../crypto_pwhash_scryptsalsa208sha256.h | 120 ++++ .../include/sodium/crypto_scalarmult.h | 46 ++ .../sodium/crypto_scalarmult_curve25519.h | 42 ++ .../sodium/crypto_scalarmult_ed25519.h | 51 ++ .../sodium/crypto_scalarmult_ristretto255.h | 43 ++ .../include/sodium/crypto_secretbox.h | 93 +++ .../crypto_secretbox_xchacha20poly1305.h | 70 ++ .../crypto_secretbox_xsalsa20poly1305.h | 69 ++ .../crypto_secretstream_xchacha20poly1305.h | 108 +++ .../include/sodium/crypto_shorthash.h | 41 ++ .../sodium/crypto_shorthash_siphash24.h | 50 ++ .../libsodium/include/sodium/crypto_sign.h | 107 +++ .../include/sodium/crypto_sign_ed25519.h | 124 ++++ .../crypto_sign_edwards25519sha512batch.h | 55 ++ .../libsodium/include/sodium/crypto_stream.h | 59 ++ .../include/sodium/crypto_stream_chacha20.h | 106 +++ .../include/sodium/crypto_stream_salsa20.h | 61 ++ .../include/sodium/crypto_stream_salsa2012.h | 53 ++ .../include/sodium/crypto_stream_salsa208.h | 56 ++ .../include/sodium/crypto_stream_xchacha20.h | 61 ++ .../include/sodium/crypto_stream_xsalsa20.h | 61 ++ .../include/sodium/crypto_verify_16.h | 23 + .../include/sodium/crypto_verify_32.h | 23 + .../include/sodium/crypto_verify_64.h | 23 + .../include/sodium/crypto_xof_shake128.h | 55 ++ .../include/sodium/crypto_xof_shake256.h | 54 ++ .../include/sodium/crypto_xof_turboshake128.h | 54 ++ .../include/sodium/crypto_xof_turboshake256.h | 54 ++ .../libsodium/include/sodium/export.h | 57 ++ .../libsodium/include/sodium/randombytes.h | 72 ++ .../sodium/randombytes_internal_random.h | 22 + .../include/sodium/randombytes_sysrandom.h | 19 + .../libsodium/include/sodium/runtime.h | 55 ++ .../libsodium/include/sodium/utils.h | 177 +++++ .../libsodium/include/sodium/version.h | 33 + .../ios-simulators/lib/libsodium-simulator.a | Bin 0 -> 1887672 bytes .../libsodium/ios/lib/libsodium.a | Bin 0 -> 1140408 bytes 83 files changed, 6649 insertions(+), 4 deletions(-) create mode 100644 quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt create mode 100644 quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt create mode 100644 quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/TestPackageClasses.kt create mode 100644 quartz/src/iosTest/resources/nip44.vectors.json create mode 100644 quartz/src/nativeInterop/libsodium/LICENSE create mode 100644 quartz/src/nativeInterop/libsodium/include/module.modulemap create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/core.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis128l.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis256.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aes256gcm.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_chacha20poly1305.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_xchacha20poly1305.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha256.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512256.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_box.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xchacha20poly1305.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xsalsa20poly1305.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ed25519.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hchacha20.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hsalsa20.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_keccak1600.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ristretto255.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa20.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa2012.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa208.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash_blake2b.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha256.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha512.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_ipcrypt.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_blake2b.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha256.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha512.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kx.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth_poly1305.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2i.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2id.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_scryptsalsa208sha256.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_curve25519.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ed25519.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ristretto255.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xchacha20poly1305.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xsalsa20poly1305.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretstream_xchacha20poly1305.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash_siphash24.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_ed25519.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_edwards25519sha512batch.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_chacha20.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa20.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa2012.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa208.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xchacha20.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xsalsa20.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_16.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_32.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_64.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake128.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake256.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake128.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake256.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/export.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/randombytes.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/randombytes_internal_random.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/randombytes_sysrandom.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/runtime.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/utils.h create mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/version.h create mode 100644 quartz/src/nativeInterop/libsodium/ios-simulators/lib/libsodium-simulator.a create mode 100644 quartz/src/nativeInterop/libsodium/ios/lib/libsodium.a diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 48ef19a911..3a5305c33b 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -5,6 +5,7 @@ import io.github.frankois944.spmForKmp.swiftPackageConfig import io.github.frankois944.spmForKmp.utils.ExperimentalSpmForKmpFeature import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest +import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile plugins { alias(libs.plugins.kotlinMultiplatform) @@ -83,19 +84,71 @@ kotlin { } } + val libsodiumPath = project.file("src/nativeInterop/libsodium").toString() + iosX64 { + compilations.getByName("main") { + val headerFilesPath = "$libsodiumPath/include/sodium" + val Clibsodium by cinterops.creating { + definitionFile.set(project.file("Clibsodium.def")) + includeDirs("$libsodiumPath/include/sodium") + + headers( + "$headerFilesPath/crypto_aead_xchacha20poly1305.h", + "$headerFilesPath/crypto_core_hchacha20.h", + "$headerFilesPath/crypto_stream_chacha20.h" + ) + } + } + binaries.all { + //linkerOpts("-L${libsodiumPath}/ios/lib", "-llibsodium") + } binaries.framework { baseName = xcfName } } iosArm64 { + compilations.getByName("main") { + val headerFilesPath = "$libsodiumPath/include/sodium" + val Clibsodium by cinterops.creating { + definitionFile.set(project.file("Clibsodium.def")) + includeDirs("$libsodiumPath/include/sodium") + + headers( + "$headerFilesPath/crypto_aead_xchacha20poly1305.h", + "$headerFilesPath/crypto_core_hchacha20.h", + "$headerFilesPath/crypto_stream_chacha20.h" + ) + } + } + binaries.all { + //linkerOpts("-L${libsodiumPath}/ios/lib", "-llibsodium") + } binaries.framework { baseName = xcfName } } iosSimulatorArm64 { + compilations.getByName("main") { + val headerFilesPath = "$libsodiumPath/include/sodium" + val Clibsodium by cinterops.creating { + definitionFile.set(project.file("Clibsodium.def")) + includeDirs("${libsodiumPath}/ios-simulators/lib") + //compilerOpts("-I${libsodiumPath}/ios-simulators/lib") + + + headers( + "$headerFilesPath/crypto_aead_xchacha20poly1305.h", + "$headerFilesPath/crypto_core_hchacha20.h", + "$headerFilesPath/crypto_stream_chacha20.h" + ) + } + } + binaries.all { + //linkerOpts("-L${libsodiumPath}/ios-simulators/lib", "-llibsodium-simulator") + } binaries.framework { baseName = xcfName } @@ -239,6 +292,8 @@ kotlin { implementation(libs.charlietap.cachemap) implementation(libs.net.thauvin.erik.urlencoder.lib) implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal) +// implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4") +// implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4") } } @@ -320,3 +375,22 @@ mavenPublishing { } } } + +tasks.register("GenerateSodiumCinteropFile") { + val libsodiumPath = project.file("src/nativeInterop/libsodium").toString() + val definitionFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") + definitionFile.writeText("package = Clibsodium") + definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") + definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") + + doLast { + executionResult.get().assertNormalExitValue() + } +} + +tasks.withType().configureEach { + val fileDefinition = project.file("src/nativeInterop/cinterop/Clibsodium.def") + if (!fileDefinition.exists()) { + dependsOn("GenerateSodiumCinteropFile") + } +} diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt index ded63960a1..a14715e889 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt @@ -20,7 +20,14 @@ */ package com.vitorpamplona.quartz.utils +import kotlinx.cinterop.CValuesRef +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.UByteVar +import kotlinx.cinterop.refTo +import kotlin.experimental.ExperimentalNativeApi + actual object LibSodiumInstance { + @OptIn(ExperimentalForeignApi::class) actual fun cryptoAeadXChaCha20Poly1305IetfDecrypt( message: ByteArray, nSec: ByteArray, @@ -29,9 +36,21 @@ actual object LibSodiumInstance { nPub: ByteArray, k: ByteArray, ): Boolean { - TODO("Not yet implemented") + val returnCode = Clibsodium.crypto_aead_xchacha20poly1305_ietf_decrypt( + m = message.uRefTo(0), + mlen_p = ulongArrayOf(message.size.toULong()).refTo(0), + nsec = nSec.uRefTo(0), + c = ciphertext.uRefTo(0), + clen = ciphertext.size.toULong(), + ad = ad.uRefTo(0), + adlen = ad.size.toULong(), + npub = nPub.uRefTo(0), + k = k.uRefTo(0) + ) + return returnCode == 0 } + @OptIn(ExperimentalForeignApi::class) actual fun cryptoAeadXChaCha20Poly1305IetfEncrypt( ciphertext: ByteArray, message: ByteArray, @@ -40,22 +59,69 @@ actual object LibSodiumInstance { nPub: ByteArray, k: ByteArray, ): Boolean { - TODO("Not yet implemented") + val retCode = Clibsodium.crypto_aead_xchacha20poly1305_ietf_encrypt( + c = ciphertext.uRefTo(0), + clen_p = ulongArrayOf(ciphertext.size.toULong()).refTo(0), + m = message.uRefTo(0), + mlen = message.size.toULong(), + ad = ad.uRefTo(0), + adlen = ad.size.toULong(), + nsec = nSec.uRefTo(0), + npub = nPub.uRefTo(0), + k = k.uRefTo(0) + ) + + return retCode == 0 } + @OptIn(ExperimentalForeignApi::class) actual fun cryptoStreamChaCha20IetfXor( message: ByteArray, nonce: ByteArray?, key: ByteArray?, ): ByteArray { - TODO("Not yet implemented") + val ciphertext = ByteArray(message.size) + Clibsodium.crypto_stream_chacha20_ietf_xor( + c = ciphertext.uRefTo(0), + m = message.uRefTo(0), + mlen = message.size.toULong(), + n = nonce?.uRefTo(0), + k = key?.uRefTo(0) + ) + return ciphertext } + @OptIn(ExperimentalForeignApi::class, ExperimentalNativeApi::class) actual fun cryptoStreamXChaCha20Xor( messageBytes: ByteArray, nonce: ByteArray, key: ByteArray, ): ByteArray { - TODO("Not yet implemented") + val cipher = ByteArray(messageBytes.size) + val k2 = ByteArray(32) + + val nonceChaCha = nonce.drop(16).toByteArray() + assert(nonceChaCha.size == 8) + + Clibsodium.crypto_core_hchacha20( + out = k2.uRefTo(0), + nonce.uRefTo(0), + k = key.uRefTo(0), + c = null + ) + + val resultCode = Clibsodium.crypto_stream_chacha20_xor_ic( + c = cipher.uRefTo(0), + m = messageBytes.uRefTo(0), + mlen = messageBytes.size.toULong(), + n = nonceChaCha.uRefTo(0), + ic = 0L.toULong(), + k = k2.uRefTo(0) + ) + return if (resultCode == 0) cipher else throw IllegalStateException("Could not decrypt message") } } + +@OptIn(ExperimentalForeignApi::class) +@Suppress("UNCHECKED_CAST") +private fun ByteArray.uRefTo(index: Int) = refTo(index) as CValuesRef diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt new file mode 100644 index 0000000000..57838941bb --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v1Test.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip44Encryption + +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01Crypto +import kotlin.test.Test +import kotlin.test.assertEquals + +class Nip44v1Test { + private val nip44v1 = Nip44v1() + + @Test + fun testSharedSecretCompatibilityWithCoracle() { + val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561" + val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f" + + val key = + nip44v1.getSharedSecret( + privateKey = privateKey.hexToByteArray(), + pubKey = publicKey.hexToByteArray(), + ) + + assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey()) + } + + @Test + fun testSharedSecret() { + val sender = KeyPair() + val receiver = KeyPair() + + val sharedSecret1 = nip44v1.getSharedSecret(sender.privKey!!, receiver.pubKey) + val sharedSecret2 = nip44v1.getSharedSecret(receiver.privKey!!, sender.pubKey) + + assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey()) + + val secretKey1 = KeyPair(privKey = sharedSecret1) + val secretKey2 = KeyPair(privKey = sharedSecret2) + + assertEquals(secretKey1.pubKey.toHexKey(), secretKey2.pubKey.toHexKey()) + assertEquals(secretKey1.privKey?.toHexKey(), secretKey2.privKey?.toHexKey()) + } + + @Test + fun encryptDecrypt() { + val msg = "Hi" + + val privateKey = Nip01Crypto.privKeyCreate() + val publicKey = Nip01Crypto.pubKeyCreate(privateKey) + + val encrypted = nip44v1.encrypt(msg, privateKey, publicKey) + val decrypted = nip44v1.decrypt(encrypted, privateKey, publicKey) + + assertEquals(msg, decrypted) + } + + @Test + fun encryptDecryptSharedSecret() { + val msg = "Hi" + + val privateKey = Nip01Crypto.privKeyCreate() + val publicKey = Nip01Crypto.pubKeyCreate(privateKey) + + val sharedSecret = nip44v1.getSharedSecret(privateKey, publicKey) + + val encrypted = nip44v1.encrypt(msg, sharedSecret) + val decrypted = nip44v1.decrypt(encrypted, sharedSecret) + + assertEquals(msg, decrypted) + } +} diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt new file mode 100644 index 0000000000..7e0b09ef42 --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2Test.kt @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip44Encryption + +import com.vitorpamplona.quartz.TestResourceLoader +import com.vitorpamplona.quartz.nip01Core.core.JsonMapper +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.crypto.Nip01Crypto +import com.vitorpamplona.quartz.utils.RandomInstance +import com.vitorpamplona.quartz.utils.sha256.sha256 +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail + +class Nip44v2Test { + private val vectors: VectorFile = + JsonMapper.jsonInstance.decodeFromString( + TestResourceLoader().loadString("nip44.vectors.json"), + ) + + private val nip44v2 = Nip44v2() + + @Test + fun conversationKeyTest() { + for (v in vectors.v2?.valid?.getConversationKey!!) { + val conversationKey = + nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), v.pub2!!.hexToByteArray()) + + assertEquals(v.conversationKey, conversationKey.toHexKey()) + } + } + + @Test + fun paddingTest() { + for (v in vectors.v2?.valid?.calcPaddedLen!!) { + val actual = nip44v2.calcPaddedLen(v[0]) + assertEquals(v[1], actual) + } + } + + @Test + fun testCompressedWith02Keys() { + val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray() + val privateKeyB = "65f039136f8da8d3e87b4818746b53318d5481e24b2673f162815144223a0b5a".hexToByteArray() + + val publicKeyA = Nip01Crypto.pubKeyCreate(privateKeyA) + val publicKeyB = Nip01Crypto.pubKeyCreate(privateKeyB) + + assertEquals( + nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(), + nip44v2.getConversationKey(privateKeyB, publicKeyA).toHexKey(), + ) + } + + @Test + fun testCompressedWith03Keys() { + val privateKeyA = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561".hexToByteArray() + val privateKeyB = "e6159851715b4aa6190c22b899b0c792847de0a4435ac5b678f35738351c43b0".hexToByteArray() + + val publicKeyA = Nip01Crypto.pubKeyCreate(privateKeyA) + val publicKeyB = Nip01Crypto.pubKeyCreate(privateKeyB) + + assertEquals( + nip44v2.getConversationKey(privateKeyA, publicKeyB).toHexKey(), + nip44v2.getConversationKey(privateKeyB, publicKeyA).toHexKey(), + ) + } + + @Test + fun encryptDecryptTest() { + for (v in vectors.v2?.valid?.encryptDecrypt!!) { + val pub2 = KeyPair(v.sec2!!.hexToByteArray()) + val conversationKey1 = nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), pub2.pubKey) + assertEquals(v.conversationKey, conversationKey1.toHexKey()) + + val ciphertext = + nip44v2 + .encryptWithNonce( + v.plaintext!!, + conversationKey1, + v.nonce!!.hexToByteArray(), + ).encodePayload() + + assertEquals(v.payload, ciphertext) + + val pub1 = KeyPair(v.sec1.hexToByteArray()) + val conversationKey2 = nip44v2.getConversationKey(v.sec2.hexToByteArray(), pub1.pubKey) + assertEquals(v.conversationKey, conversationKey2.toHexKey()) + + val decrypted = nip44v2.decrypt(v.payload!!, conversationKey2) + assertEquals(v.plaintext, decrypted) + } + } + + @Test + fun encryptDecryptLongTest() { + for (v in vectors.v2?.valid?.encryptDecryptLongMsg!!) { + val conversationKey = v.conversationKey!!.hexToByteArray() + val plaintext = v.pattern!!.repeat(v.repeat!!) + + assertEquals(v.plaintextSha256, sha256Hex(plaintext.encodeToByteArray())) + + val ciphertext = + nip44v2 + .encryptWithNonce( + plaintext, + conversationKey, + v.nonce!!.hexToByteArray(), + ).encodePayload() + + assertEquals(v.payloadSha256, sha256Hex(ciphertext.encodeToByteArray())) + + val decrypted = nip44v2.decrypt(ciphertext, conversationKey) + + assertEquals(plaintext, decrypted) + } + } + + @Test + fun extendedMessageLengths() { + for (v in vectors.v2?.invalid?.encryptMsgLengths!!) { + val key = RandomInstance.bytes(32) + try { + val input = "a".repeat(v) + val result = nip44v2.encrypt(input, key) + val decrypted = nip44v2.decrypt(result, key) + assertEquals(input, decrypted) + } catch (e: Exception) { + assertNotNull(e) + } + } + } + + @Test + fun invalidDecrypt() { + for (v in vectors.v2?.invalid?.decrypt!!) { + try { + val result = nip44v2.decrypt(v.payload!!, v.conversationKey!!.hexToByteArray()) + assertNull(result) + // fail("Should Throw for ${v.note}") + } catch (e: Exception) { + assertNotNull(e) + } + } + } + + @Test + fun invalidConversationKey() { + for (v in vectors.v2?.invalid?.getConversationKey!!) { + try { + nip44v2.getConversationKey(v.sec1!!.hexToByteArray(), v.pub2!!.hexToByteArray()) + fail("Should Throw for ${v.note}") + } catch (e: Exception) { + assertNotNull(e) + } + } + } + + private fun sha256Hex(data: ByteArray) = sha256(data).toHexKey() +} diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/TestPackageClasses.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/TestPackageClasses.kt new file mode 100644 index 0000000000..4eccbf19bb --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/TestPackageClasses.kt @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip44Encryption + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class VectorFile( + val v2: V2? = V2(), +) + +@Serializable +data class V2( + val valid: Valid? = Valid(), + val invalid: Invalid? = Invalid(), +) + +@Serializable +data class Valid( + @SerialName("get_conversation_key") val getConversationKey: ArrayList = arrayListOf(), + @SerialName("get_message_keys") val getMessageKeys: GetMessageKeys? = GetMessageKeys(), + @SerialName("calc_padded_len") val calcPaddedLen: ArrayList> = arrayListOf(), + @SerialName("encrypt_decrypt") val encryptDecrypt: ArrayList = arrayListOf(), + @SerialName("encrypt_decrypt_long_msg") + val encryptDecryptLongMsg: ArrayList = arrayListOf(), +) + +@Serializable +data class Invalid( + @SerialName("encrypt_msg_lengths") val encryptMsgLengths: ArrayList = arrayListOf(), + @SerialName("get_conversation_key") val getConversationKey: ArrayList = arrayListOf(), + @SerialName("decrypt") val decrypt: ArrayList = arrayListOf(), +) + +@Serializable +data class GetConversationKey( + val sec1: String? = null, + val pub2: String? = null, + val note: String? = null, + @SerialName("conversation_key") val conversationKey: String? = null, +) + +@Serializable +data class GetMessageKeys( + @SerialName("conversation_key") val conversationKey: String? = null, + val keys: ArrayList = arrayListOf(), +) + +@Serializable +data class Keys( + @SerialName("nonce") val nonce: String? = null, + @SerialName("chacha_key") val chachaKey: String? = null, + @SerialName("chacha_nonce") val chachaNonce: String? = null, + @SerialName("hmac_key") val hmacKey: String? = null, +) + +@Serializable +data class EncryptDecrypt( + val sec1: String? = null, + val sec2: String? = null, + @SerialName("conversation_key") val conversationKey: String? = null, + val nonce: String? = null, + val plaintext: String? = null, + val payload: String? = null, +) + +@Serializable +data class EncryptDecryptLongMsg( + @SerialName("conversation_key") val conversationKey: String? = null, + val nonce: String? = null, + val pattern: String? = null, + val repeat: Int? = null, + @SerialName("plaintext_sha256") val plaintextSha256: String? = null, + @SerialName("payload_sha256") val payloadSha256: String? = null, +) + +@Serializable +data class Decrypt( + @SerialName("conversation_key") val conversationKey: String? = null, + val nonce: String? = null, + val plaintext: String? = null, + val payload: String? = null, + val note: String? = null, +) diff --git a/quartz/src/iosTest/resources/nip44.vectors.json b/quartz/src/iosTest/resources/nip44.vectors.json new file mode 100644 index 0000000000..4d105420f3 --- /dev/null +++ b/quartz/src/iosTest/resources/nip44.vectors.json @@ -0,0 +1,664 @@ +{ + "v2": { + "valid": { + "get_conversation_key": [ + { + "sec1": "315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268", + "pub2": "c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133", + "conversation_key": "3dfef0ce2a4d80a25e7a328accf73448ef67096f65f79588e358d9a0eb9013f1" + }, + { + "sec1": "a1e37752c9fdc1273be53f68c5f74be7c8905728e8de75800b94262f9497c86e", + "pub2": "03bb7947065dde12ba991ea045132581d0954f042c84e06d8c00066e23c1a800", + "conversation_key": "4d14f36e81b8452128da64fe6f1eae873baae2f444b02c950b90e43553f2178b" + }, + { + "sec1": "98a5902fd67518a0c900f0fb62158f278f94a21d6f9d33d30cd3091195500311", + "pub2": "aae65c15f98e5e677b5050de82e3aba47a6fe49b3dab7863cf35d9478ba9f7d1", + "conversation_key": "9c00b769d5f54d02bf175b7284a1cbd28b6911b06cda6666b2243561ac96bad7" + }, + { + "sec1": "86ae5ac8034eb2542ce23ec2f84375655dab7f836836bbd3c54cefe9fdc9c19f", + "pub2": "59f90272378089d73f1339710c02e2be6db584e9cdbe86eed3578f0c67c23585", + "conversation_key": "19f934aafd3324e8415299b64df42049afaa051c71c98d0aa10e1081f2e3e2ba" + }, + { + "sec1": "2528c287fe822421bc0dc4c3615878eb98e8a8c31657616d08b29c00ce209e34", + "pub2": "f66ea16104c01a1c532e03f166c5370a22a5505753005a566366097150c6df60", + "conversation_key": "c833bbb292956c43366145326d53b955ffb5da4e4998a2d853611841903f5442" + }, + { + "sec1": "49808637b2d21129478041813aceb6f2c9d4929cd1303cdaf4fbdbd690905ff2", + "pub2": "74d2aab13e97827ea21baf253ad7e39b974bb2498cc747cdb168582a11847b65", + "conversation_key": "4bf304d3c8c4608864c0fe03890b90279328cd24a018ffa9eb8f8ccec06b505d" + }, + { + "sec1": "af67c382106242c5baabf856efdc0629cc1c5b4061f85b8ceaba52aa7e4b4082", + "pub2": "bdaf0001d63e7ec994fad736eab178ee3c2d7cfc925ae29f37d19224486db57b", + "conversation_key": "a3a575dd66d45e9379904047ebfb9a7873c471687d0535db00ef2daa24b391db" + }, + { + "sec1": "0e44e2d1db3c1717b05ffa0f08d102a09c554a1cbbf678ab158b259a44e682f1", + "pub2": "1ffa76c5cc7a836af6914b840483726207cb750889753d7499fb8b76aa8fe0de", + "conversation_key": "a39970a667b7f861f100e3827f4adbf6f464e2697686fe1a81aeda817d6b8bdf" + }, + { + "sec1": "5fc0070dbd0666dbddc21d788db04050b86ed8b456b080794c2a0c8e33287bb6", + "pub2": "31990752f296dd22e146c9e6f152a269d84b241cc95bb3ff8ec341628a54caf0", + "conversation_key": "72c21075f4b2349ce01a3e604e02a9ab9f07e35dd07eff746de348b4f3c6365e" + }, + { + "sec1": "1b7de0d64d9b12ddbb52ef217a3a7c47c4362ce7ea837d760dad58ab313cba64", + "pub2": "24383541dd8083b93d144b431679d70ef4eec10c98fceef1eff08b1d81d4b065", + "conversation_key": "dd152a76b44e63d1afd4dfff0785fa07b3e494a9e8401aba31ff925caeb8f5b1" + }, + { + "sec1": "df2f560e213ca5fb33b9ecde771c7c0cbd30f1cf43c2c24de54480069d9ab0af", + "pub2": "eeea26e552fc8b5e377acaa03e47daa2d7b0c787fac1e0774c9504d9094c430e", + "conversation_key": "770519e803b80f411c34aef59c3ca018608842ebf53909c48d35250bd9323af6" + }, + { + "sec1": "cffff919fcc07b8003fdc63bc8a00c0f5dc81022c1c927c62c597352190d95b9", + "pub2": "eb5c3cca1a968e26684e5b0eb733aecfc844f95a09ac4e126a9e58a4e4902f92", + "conversation_key": "46a14ee7e80e439ec75c66f04ad824b53a632b8409a29bbb7c192e43c00bb795" + }, + { + "sec1": "64ba5a685e443e881e9094647ddd32db14444bb21aa7986beeba3d1c4673ba0a", + "pub2": "50e6a4339fac1f3bf86f2401dd797af43ad45bbf58e0801a7877a3984c77c3c4", + "conversation_key": "968b9dbbfcede1664a4ca35a5d3379c064736e87aafbf0b5d114dff710b8a946" + }, + { + "sec1": "dd0c31ccce4ec8083f9b75dbf23cc2878e6d1b6baa17713841a2428f69dee91a", + "pub2": "b483e84c1339812bed25be55cff959778dfc6edde97ccd9e3649f442472c091b", + "conversation_key": "09024503c7bde07eb7865505891c1ea672bf2d9e25e18dd7a7cea6c69bf44b5d" + }, + { + "sec1": "af71313b0d95c41e968a172b33ba5ebd19d06cdf8a7a98df80ecf7af4f6f0358", + "pub2": "2a5c25266695b461ee2af927a6c44a3c598b8095b0557e9bd7f787067435bc7c", + "conversation_key": "fe5155b27c1c4b4e92a933edae23726a04802a7cc354a77ac273c85aa3c97a92" + }, + { + "sec1": "6636e8a389f75fe068a03b3edb3ea4a785e2768e3f73f48ffb1fc5e7cb7289dc", + "pub2": "514eb2064224b6a5829ea21b6e8f7d3ea15ff8e70e8555010f649eb6e09aec70", + "conversation_key": "ff7afacd4d1a6856d37ca5b546890e46e922b508639214991cf8048ddbe9745c" + }, + { + "sec1": "94b212f02a3cfb8ad147d52941d3f1dbe1753804458e6645af92c7b2ea791caa", + "pub2": "f0cac333231367a04b652a77ab4f8d658b94e86b5a8a0c472c5c7b0d4c6a40cc", + "conversation_key": "e292eaf873addfed0a457c6bd16c8effde33d6664265697f69f420ab16f6669b" + }, + { + "sec1": "aa61f9734e69ae88e5d4ced5aae881c96f0d7f16cca603d3bed9eec391136da6", + "pub2": "4303e5360a884c360221de8606b72dd316da49a37fe51e17ada4f35f671620a6", + "conversation_key": "8e7d44fd4767456df1fb61f134092a52fcd6836ebab3b00766e16732683ed848" + }, + { + "sec1": "5e914bdac54f3f8e2cba94ee898b33240019297b69e96e70c8a495943a72fc98", + "pub2": "5bd097924f606695c59f18ff8fd53c174adbafaaa71b3c0b4144a3e0a474b198", + "conversation_key": "f5a0aecf2984bf923c8cd5e7bb8be262d1a8353cb93959434b943a07cf5644bc" + }, + { + "sec1": "8b275067add6312ddee064bcdbeb9d17e88aa1df36f430b2cea5cc0413d8278a", + "pub2": "65bbbfca819c90c7579f7a82b750a18c858db1afbec8f35b3c1e0e7b5588e9b8", + "conversation_key": "2c565e7027eb46038c2263563d7af681697107e975e9914b799d425effd248d6" + }, + { + "sec1": "1ac848de312285f85e0f7ec208aac20142a1f453402af9b34ec2ec7a1f9c96fc", + "pub2": "45f7318fe96034d23ee3ddc25b77f275cc1dd329664dd51b89f89c4963868e41", + "conversation_key": "b56e970e5057a8fd929f8aad9248176b9af87819a708d9ddd56e41d1aec74088" + }, + { + "sec1": "295a1cf621de401783d29d0e89036aa1c62d13d9ad307161b4ceb535ba1b40e6", + "pub2": "840115ddc7f1034d3b21d8e2103f6cb5ab0b63cf613f4ea6e61ae3d016715cdd", + "conversation_key": "b4ee9c0b9b9fef88975773394f0a6f981ca016076143a1bb575b9ff46e804753" + }, + { + "sec1": "a28eed0fe977893856ab9667e06ace39f03abbcdb845c329a1981be438ba565d", + "pub2": "b0f38b950a5013eba5ab4237f9ed29204a59f3625c71b7e210fec565edfa288c", + "conversation_key": "9d3a802b45bc5aeeb3b303e8e18a92ddd353375710a31600d7f5fff8f3a7285b" + }, + { + "sec1": "7ab65af72a478c05f5c651bdc4876c74b63d20d04cdbf71741e46978797cd5a4", + "pub2": "f1112159161b568a9cb8c9dd6430b526c4204bcc8ce07464b0845b04c041beda", + "conversation_key": "943884cddaca5a3fef355e9e7f08a3019b0b66aa63ec90278b0f9fdb64821e79" + }, + { + "sec1": "95c79a7b75ba40f2229e85756884c138916f9d103fc8f18acc0877a7cceac9fe", + "pub2": "cad76bcbd31ca7bbda184d20cc42f725ed0bb105b13580c41330e03023f0ffb3", + "conversation_key": "81c0832a669eea13b4247c40be51ccfd15bb63fcd1bba5b4530ce0e2632f301b" + }, + { + "sec1": "baf55cc2febd4d980b4b393972dfc1acf49541e336b56d33d429bce44fa12ec9", + "pub2": "0c31cf87fe565766089b64b39460ebbfdedd4a2bc8379be73ad3c0718c912e18", + "conversation_key": "37e2344da9ecdf60ae2205d81e89d34b280b0a3f111171af7e4391ded93b8ea6" + }, + { + "sec1": "6eeec45acd2ed31693c5256026abf9f072f01c4abb61f51cf64e6956b6dc8907", + "pub2": "e501b34ed11f13d816748c0369b0c728e540df3755bab59ed3327339e16ff828", + "conversation_key": "afaa141b522ddb27bb880d768903a7f618bb8b6357728cae7fb03af639b946e6" + }, + { + "sec1": "261a076a9702af1647fb343c55b3f9a4f1096273002287df0015ba81ce5294df", + "pub2": "b2777c863878893ae100fb740c8fab4bebd2bf7be78c761a75593670380a6112", + "conversation_key": "76f8d2853de0734e51189ced523c09427c3e46338b9522cd6f74ef5e5b475c74" + }, + { + "sec1": "ed3ec71ca406552ea41faec53e19f44b8f90575eda4b7e96380f9cc73c26d6f3", + "pub2": "86425951e61f94b62e20cae24184b42e8e17afcf55bafa58645efd0172624fae", + "conversation_key": "f7ffc520a3a0e9e9b3c0967325c9bf12707f8e7a03f28b6cd69ae92cf33f7036" + }, + { + "sec1": "5a788fc43378d1303ac78639c59a58cb88b08b3859df33193e63a5a3801c722e", + "pub2": "a8cba2f87657d229db69bee07850fd6f7a2ed070171a06d006ec3a8ac562cf70", + "conversation_key": "7d705a27feeedf78b5c07283362f8e361760d3e9f78adab83e3ae5ce7aeb6409" + }, + { + "sec1": "63bffa986e382b0ac8ccc1aa93d18a7aa445116478be6f2453bad1f2d3af2344", + "pub2": "b895c70a83e782c1cf84af558d1038e6b211c6f84ede60408f519a293201031d", + "conversation_key": "3a3b8f00d4987fc6711d9be64d9c59cf9a709c6c6481c2cde404bcc7a28f174e" + }, + { + "sec1": "e4a8bcacbf445fd3721792b939ff58e691cdcba6a8ba67ac3467b45567a03e5c", + "pub2": "b54053189e8c9252c6950059c783edb10675d06d20c7b342f73ec9fa6ed39c9d", + "conversation_key": "7b3933b4ef8189d347169c7955589fc1cfc01da5239591a08a183ff6694c44ad" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "pub2": "0000000000000000000000000000000000000000000000000000000000000002", + "conversation_key": "8b6392dbf2ec6a2b2d5b1477fc2be84d63ef254b667cadd31bd3f444c44ae6ba", + "note": "sec1 = n-2, pub2: random, 0x02" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb", + "conversation_key": "be234f46f60a250bef52a5ee34c758800c4ca8e5030bf4cc1a31d37ba2104d43", + "note": "sec1 = 2, pub2: rand" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "conversation_key": "3b4610cb7189beb9cc29eb3716ecc6102f1247e8f3101a03a1787d8908aeb54e", + "note": "sec1 == pub2" + } + ], + "get_message_keys": { + "conversation_key": "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54", + "keys": [ + { + "nonce": "e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72", + "chacha_key": "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76", + "chacha_nonce": "c4ad129bb01180c0933a160c", + "hmac_key": "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4" + }, + { + "nonce": "e1d6d28c46de60168b43d79dacc519698512ec35e8ccb12640fc8e9f26121101", + "chacha_key": "e35b88f8d4a8f1606c5082f7a64b100e5d85fcdb2e62aeafbec03fb9e860ad92", + "chacha_nonce": "22925e920cee4a50a478be90", + "hmac_key": "46a7c55d4283cb0df1d5e29540be67abfe709e3b2e14b7bf9976e6df994ded30" + }, + { + "nonce": "cfc13bef512ac9c15951ab00030dfaf2626fdca638dedb35f2993a9eeb85d650", + "chacha_key": "020783eb35fdf5b80ef8c75377f4e937efb26bcbad0e61b4190e39939860c4bf", + "chacha_nonce": "d3594987af769a52904656ac", + "hmac_key": "237ec0ccb6ebd53d179fa8fd319e092acff599ef174c1fdafd499ef2b8dee745" + }, + { + "nonce": "ea6eb84cac23c5c1607c334e8bdf66f7977a7e374052327ec28c6906cbe25967", + "chacha_key": "ff68db24b34fa62c78ac5ffeeaf19533afaedf651fb6a08384e46787f6ce94be", + "chacha_nonce": "50bb859aa2dde938cc49ec7a", + "hmac_key": "06ff32e1f7b29753a727d7927b25c2dd175aca47751462d37a2039023ec6b5a6" + }, + { + "nonce": "8c2e1dd3792802f1f9f7842e0323e5d52ad7472daf360f26e15f97290173605d", + "chacha_key": "2f9daeda8683fdeede81adac247c63cc7671fa817a1fd47352e95d9487989d8b", + "chacha_nonce": "400224ba67fc2f1b76736916", + "hmac_key": "465c05302aeeb514e41c13ed6405297e261048cfb75a6f851ffa5b445b746e4b" + }, + { + "nonce": "05c28bf3d834fa4af8143bf5201a856fa5fac1a3aee58f4c93a764fc2f722367", + "chacha_key": "1e3d45777025a035be566d80fd580def73ed6f7c043faec2c8c1c690ad31c110", + "chacha_nonce": "021905b1ea3afc17cb9bf96f", + "hmac_key": "74a6e481a89dcd130aaeb21060d7ec97ad30f0007d2cae7b1b11256cc70dfb81" + }, + { + "nonce": "5e043fb153227866e75a06d60185851bc90273bfb93342f6632a728e18a07a17", + "chacha_key": "1ea72c9293841e7737c71567d8120145a58991aaa1c436ef77bf7adb83f882f1", + "chacha_nonce": "72f69a5a5f795465cee59da8", + "hmac_key": "e9daa1a1e9a266ecaa14e970a84bce3fbbf329079bbccda626582b4e66a0d4c9" + }, + { + "nonce": "7be7338eaf06a87e274244847fe7a97f5c6a91f44adc18fcc3e411ad6f786dbf", + "chacha_key": "881e7968a1f0c2c80742ee03cd49ea587e13f22699730f1075ade01931582bf6", + "chacha_nonce": "6e69be92d61c04a276021565", + "hmac_key": "901afe79e74b19967c8829af23617d7d0ffbf1b57190c096855c6a03523a971b" + }, + { + "nonce": "94571c8d590905bad7becd892832b472f2aa5212894b6ce96e5ba719c178d976", + "chacha_key": "f80873dd48466cb12d46364a97b8705c01b9b4230cb3ec3415a6b9551dc42eef", + "chacha_nonce": "3dda53569cfcb7fac1805c35", + "hmac_key": "e9fc264345e2839a181affebc27d2f528756e66a5f87b04bf6c5f1997047051e" + }, + { + "nonce": "13a6ee974b1fd759135a2c2010e3cdda47081c78e771125e4f0c382f0284a8cb", + "chacha_key": "bc5fb403b0bed0d84cf1db872b6522072aece00363178c98ad52178d805fca85", + "chacha_nonce": "65064239186e50304cc0f156", + "hmac_key": "e872d320dde4ed3487958a8e43b48aabd3ced92bc24bb8ff1ccb57b590d9701a" + }, + { + "nonce": "082fecdb85f358367b049b08be0e82627ae1d8edb0f27327ccb593aa2613b814", + "chacha_key": "1fbdb1cf6f6ea816349baf697932b36107803de98fcd805ebe9849b8ad0e6a45", + "chacha_nonce": "2e605e1d825a3eaeb613db9c", + "hmac_key": "fae910f591cf3c7eb538c598583abad33bc0a03085a96ca4ea3a08baf17c0eec" + }, + { + "nonce": "4c19020c74932c30ec6b2d8cd0d5bb80bd0fc87da3d8b4859d2fb003810afd03", + "chacha_key": "1ab9905a0189e01cda82f843d226a82a03c4f5b6dbea9b22eb9bc953ba1370d4", + "chacha_nonce": "cbb2530ea653766e5a37a83a", + "hmac_key": "267f68acac01ac7b34b675e36c2cef5e7b7a6b697214add62a491bedd6efc178" + }, + { + "nonce": "67723a3381497b149ce24814eddd10c4c41a1e37e75af161930e6b9601afd0ff", + "chacha_key": "9ecbd25e7e2e6c97b8c27d376dcc8c5679da96578557e4e21dba3a7ef4e4ac07", + "chacha_nonce": "ef649fcf335583e8d45e3c2e", + "hmac_key": "04dbbd812fa8226fdb45924c521a62e3d40a9e2b5806c1501efdeba75b006bf1" + }, + { + "nonce": "42063fe80b093e8619b1610972b4c3ab9e76c14fd908e642cd4997cafb30f36c", + "chacha_key": "211c66531bbcc0efcdd0130f9f1ebc12a769105eb39608994bcb188fa6a73a4a", + "chacha_nonce": "67803605a7e5010d0f63f8c8", + "hmac_key": "e840e4e8921b57647369d121c5a19310648105dbdd008200ebf0d3b668704ff8" + }, + { + "nonce": "b5ac382a4be7ac03b554fe5f3043577b47ea2cd7cfc7e9ca010b1ffbb5cf1a58", + "chacha_key": "b3b5f14f10074244ee42a3837a54309f33981c7232a8b16921e815e1f7d1bb77", + "chacha_nonce": "4e62a0073087ed808be62469", + "hmac_key": "c8efa10230b5ea11633816c1230ca05fa602ace80a7598916d83bae3d3d2ccd7" + }, + { + "nonce": "e9d1eba47dd7e6c1532dc782ff63125db83042bb32841db7eeafd528f3ea7af9", + "chacha_key": "54241f68dc2e50e1db79e892c7c7a471856beeb8d51b7f4d16f16ab0645d2f1a", + "chacha_nonce": "a963ed7dc29b7b1046820a1d", + "hmac_key": "aba215c8634530dc21c70ddb3b3ee4291e0fa5fa79be0f85863747bde281c8b2" + }, + { + "nonce": "a94ecf8efeee9d7068de730fad8daf96694acb70901d762de39fa8a5039c3c49", + "chacha_key": "c0565e9e201d2381a2368d7ffe60f555223874610d3d91fbbdf3076f7b1374dd", + "chacha_nonce": "329bb3024461e84b2e1c489b", + "hmac_key": "ac42445491f092481ce4fa33b1f2274700032db64e3a15014fbe8c28550f2fec" + }, + { + "nonce": "533605ea214e70c25e9a22f792f4b78b9f83a18ab2103687c8a0075919eaaa53", + "chacha_key": "ab35a5e1e54d693ff023db8500d8d4e79ad8878c744e0eaec691e96e141d2325", + "chacha_nonce": "653d759042b85194d4d8c0a7", + "hmac_key": "b43628e37ba3c31ce80576f0a1f26d3a7c9361d29bb227433b66f49d44f167ba" + }, + { + "nonce": "7f38df30ceea1577cb60b355b4f5567ff4130c49e84fed34d779b764a9cc184c", + "chacha_key": "a37d7f211b84a551a127ff40908974eb78415395d4f6f40324428e850e8c42a3", + "chacha_nonce": "b822e2c959df32b3cb772a7c", + "hmac_key": "1ba31764f01f69b5c89ded2d7c95828e8052c55f5d36f1cd535510d61ba77420" + }, + { + "nonce": "11b37f9dbc4d0185d1c26d5f4ed98637d7c9701fffa65a65839fa4126573a4e5", + "chacha_key": "964f38d3a31158a5bfd28481247b18dd6e44d69f30ba2a40f6120c6d21d8a6ba", + "chacha_nonce": "5f72c5b87c590bcd0f93b305", + "hmac_key": "2fc4553e7cedc47f29690439890f9f19c1077ef3e9eaeef473d0711e04448918" + }, + { + "nonce": "8be790aa483d4cdd843189f71f135b3ec7e31f381312c8fe9f177aab2a48eafa", + "chacha_key": "95c8c74d633721a131316309cf6daf0804d59eaa90ea998fc35bac3d2fbb7a94", + "chacha_nonce": "409a7654c0e4bf8c2c6489be", + "hmac_key": "21bb0b06eb2b460f8ab075f497efa9a01c9cf9146f1e3986c3bf9da5689b6dc4" + }, + { + "nonce": "19fd2a718ea084827d6bd73f509229ddf856732108b59fc01819f611419fd140", + "chacha_key": "cc6714b9f5616c66143424e1413d520dae03b1a4bd202b82b0a89b0727f5cdc8", + "chacha_nonce": "1b7fd2534f015a8f795d8f32", + "hmac_key": "2bef39c4ce5c3c59b817e86351373d1554c98bc131c7e461ed19d96cfd6399a0" + }, + { + "nonce": "3c2acd893952b2f6d07d8aea76f545ca45961a93fe5757f6a5a80811d5e0255d", + "chacha_key": "c8de6c878cb469278d0af894bc181deb6194053f73da5014c2b5d2c8db6f2056", + "chacha_nonce": "6ffe4f1971b904a1b1a81b99", + "hmac_key": "df1cd69dd3646fca15594284744d4211d70e7d8472e545d276421fbb79559fd4" + }, + { + "nonce": "7dbea4cead9ac91d4137f1c0a6eebb6ba0d1fb2cc46d829fbc75f8d86aca6301", + "chacha_key": "c8e030f6aa680c3d0b597da9c92bb77c21c4285dd620c5889f9beba7446446b0", + "chacha_nonce": "a9b5a67d081d3b42e737d16f", + "hmac_key": "355a85f551bc3cce9a14461aa60994742c9bbb1c81a59ca102dc64e61726ab8e" + }, + { + "nonce": "45422e676cdae5f1071d3647d7a5f1f5adafb832668a578228aa1155a491f2f3", + "chacha_key": "758437245f03a88e2c6a32807edfabff51a91c81ca2f389b0b46f2c97119ea90", + "chacha_nonce": "263830a065af33d9c6c5aa1f", + "hmac_key": "7c581cf3489e2de203a95106bfc0de3d4032e9d5b92b2b61fb444acd99037e17" + }, + { + "nonce": "babc0c03fad24107ad60678751f5db2678041ff0d28671ede8d65bdf7aa407e9", + "chacha_key": "bd68a28bd48d9ffa3602db72c75662ac2848a0047a313d2ae2d6bc1ac153d7e9", + "chacha_nonce": "d0f9d2a1ace6c758f594ffdd", + "hmac_key": "eb435e3a642adfc9d59813051606fc21f81641afd58ea6641e2f5a9f123bb50a" + }, + { + "nonce": "7a1b8aac37d0d20b160291fad124ab697cfca53f82e326d78fef89b4b0ea8f83", + "chacha_key": "9e97875b651a1d30d17d086d1e846778b7faad6fcbc12e08b3365d700f62e4fe", + "chacha_nonce": "ccdaad5b3b7645be430992eb", + "hmac_key": "6f2f55cf35174d75752f63c06cc7cbc8441759b142999ed2d5a6d09d263e1fc4" + }, + { + "nonce": "8370e4e32d7e680a83862cab0da6136ef607014d043e64cdf5ecc0c4e20b3d9a", + "chacha_key": "1472bed5d19db9c546106de946e0649cd83cc9d4a66b087a65906e348dcf92e2", + "chacha_nonce": "ed02dece5fc3a186f123420b", + "hmac_key": "7b3f7739f49d30c6205a46b174f984bb6a9fc38e5ccfacef2dac04fcbd3b184e" + }, + { + "nonce": "9f1c5e8a29cd5677513c2e3a816551d6833ee54991eb3f00d5b68096fc8f0183", + "chacha_key": "5e1a7544e4d4dafe55941fcbdf326f19b0ca37fc49c4d47e9eec7fb68cde4975", + "chacha_nonce": "7d9acb0fdc174e3c220f40de", + "hmac_key": "e265ab116fbbb86b2aefc089a0986a0f5b77eda50c7410404ad3b4f3f385c7a7" + }, + { + "nonce": "c385aa1c37c2bfd5cc35fcdbdf601034d39195e1cabff664ceb2b787c15d0225", + "chacha_key": "06bf4e60677a13e54c4a38ab824d2ef79da22b690da2b82d0aa3e39a14ca7bdd", + "chacha_nonce": "26b450612ca5e905b937e147", + "hmac_key": "22208152be2b1f5f75e6bfcc1f87763d48bb7a74da1be3d102096f257207f8b3" + }, + { + "nonce": "3ff73528f88a50f9d35c0ddba4560bacee5b0462d0f4cb6e91caf41847040ce4", + "chacha_key": "850c8a17a23aa761d279d9901015b2bbdfdff00adbf6bc5cf22bd44d24ecabc9", + "chacha_nonce": "4a296a1fb0048e5020d3b129", + "hmac_key": "b1bf49a533c4da9b1d629b7ff30882e12d37d49c19abd7b01b7807d75ee13806" + }, + { + "nonce": "2dcf39b9d4c52f1cb9db2d516c43a7c6c3b8c401f6a4ac8f131a9e1059957036", + "chacha_key": "17f8057e6156ba7cc5310d01eda8c40f9aa388f9fd1712deb9511f13ecc37d27", + "chacha_nonce": "a8188daff807a1182200b39d", + "hmac_key": "47b89da97f68d389867b5d8a2d7ba55715a30e3d88a3cc11f3646bc2af5580ef" + } + ] + }, + "calc_padded_len": [ + [16, 32], + [32, 32], + [33, 64], + [37, 64], + [45, 64], + [49, 64], + [64, 64], + [65, 96], + [100, 128], + [111, 128], + [200, 224], + [250, 256], + [320, 320], + [383, 384], + [384, 384], + [400, 448], + [500, 512], + [512, 512], + [515, 640], + [700, 768], + [800, 896], + [900, 1024], + [1020, 1024], + [65536, 65536] + ], + "encrypt_decrypt": [ + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000001", + "sec2": "0000000000000000000000000000000000000000000000000000000000000002", + "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", + "nonce": "0000000000000000000000000000000000000000000000000000000000000001", + "plaintext": "a", + "payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "sec2": "0000000000000000000000000000000000000000000000000000000000000001", + "conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d", + "nonce": "f00000000000000000000000000000f00000000000000000000000000000000f", + "plaintext": "🍕🫃", + "payload": "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPSKSK6is9ngkX2+cSq85Th16oRTISAOfhStnixqZziKMDvB0QQzgFZdjLTPicCJaV8nDITO+QfaQ61+KbWQIOO2Yj" + }, + { + "sec1": "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a", + "sec2": "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d", + "conversation_key": "3e2b52a63be47d34fe0a80e34e73d436d6963bc8f39827f327057a9986c20a45", + "nonce": "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b", + "plaintext": "表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀", + "payload": "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7s7JqlCMJBAIIjfkpHReBPXeoMCyuClwgbT419jUWU1PwaNl4FEQYKCDKVJz+97Mp3K+Q2YGa77B6gpxB/lr1QgoqpDf7wDVrDmOqGoiPjWDqy8KzLueKDcm9BVP8xeTJIxs=" + }, + { + "sec1": "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c", + "sec2": "b9b0a1e9cc20100c5faa3bbe2777303d25950616c4c6a3fa2e3e046f936ec2ba", + "conversation_key": "d5a2f879123145a4b291d767428870f5a8d9e5007193321795b40183d4ab8c2b", + "nonce": "b20989adc3ddc41cd2c435952c0d59a91315d8c5218d5040573fc3749543acaf", + "plaintext": "ability🤝的 ȺȾ", + "payload": "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvbG2S0x4Yu86QGwPTy7mP3961I1XqB6SFFTzqDZZavhxoWMj7mEVGMQIsh2RLWI5EYQaQDIePSnXPlzf7CIt+voTD" + }, + { + "sec1": "875adb475056aec0b4809bd2db9aa00cff53a649e7b59d8edcbf4e6330b0995c", + "sec2": "9c05781112d5b0a2a7148a222e50e0bd891d6b60c5483f03456e982185944aae", + "conversation_key": "3b15c977e20bfe4b8482991274635edd94f366595b1a3d2993515705ca3cedb8", + "nonce": "8d4442713eb9d4791175cb040d98d6fc5be8864d6ec2f89cf0895a2b2b72d1b1", + "plaintext": "pepper👀їжак", + "payload": "Ao1EQnE+udR5EXXLBA2Y1vxb6IZNbsL4nPCJWisrctGxY3AduCS+jTUgAAnfvKafkmpy15+i9YMwCdccisRa8SvzW671T2JO4LFSPX31K4kYUKelSAdSPwe9NwO6LhOsnoJ+" + }, + { + "sec1": "eba1687cab6a3101bfc68fd70f214aa4cc059e9ec1b79fdb9ad0a0a4e259829f", + "sec2": "dff20d262bef9dfd94666548f556393085e6ea421c8af86e9d333fa8747e94b3", + "conversation_key": "4f1538411098cf11c8af216836444787c462d47f97287f46cf7edb2c4915b8a5", + "nonce": "2180b52ae645fcf9f5080d81b1f0b5d6f2cd77ff3c986882bb549158462f3407", + "plaintext": "( ͡° ͜ʖ ͡°)", + "payload": "AiGAtSrmRfz59QgNgbHwtdbyzXf/PJhogrtUkVhGLzQHv4qhKQwnFQ54OjVMgqCea/Vj0YqBSdhqNR777TJ4zIUk7R0fnizp6l1zwgzWv7+ee6u+0/89KIjY5q1wu6inyuiv" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "nonce": "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68", + "plaintext": "مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،", + "payload": "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItoIBsWu1CB+sStla2M4VeANASHxM78i1CfHQQH1YbBy24Tng7emYW44ol6QkFD6D8Zq7QPl+8L1c47lx8RoODEQMvNCbOk5ffUV3/AhONHBXnffrI+0025c+uRGzfqpYki4lBqm9iYU+k3Tvjczq9wU0mkVDEaM34WiQi30MfkJdRbeeYaq6kNvGPunLb3xdjjs5DL720d61Flc5ZfoZm+CBhADy9D9XiVZYLKAlkijALJur9dATYKci6OBOoc2SJS2Clai5hOVzR0yVeyHRgRfH9aLSlWW5dXcUxTo7qqRjNf8W5+J4jF4gNQp5f5d0YA4vPAzjBwSP/5bGzNDslKfcAH" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "nonce": "38d1ca0abef9e5f564e89761a86cee04574b6825d3ef2063b10ad75899e4b023", + "plaintext": "الكل في المجمو عة (5)", + "payload": "AjjRygq++eX1ZOiXYahs7gRXS2gl0+8gY7EK11iZ5LAjbOTrlfrxak5Lki42v2jMPpLSicy8eHjsWkkMtF0i925vOaKG/ZkMHh9ccQBdfTvgEGKzztedqDCAWb5TP1YwU1PsWaiiqG3+WgVvJiO4lUdMHXL7+zKKx8bgDtowzz4QAwI=" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "nonce": "4f1a31909f3483a9e69c8549a55bbc9af25fa5bbecf7bd32d9896f83ef2e12e0", + "plaintext": "𝖑𝖆𝖟𝖞 社會科學院語學研究所", + "payload": "Ak8aMZCfNIOp5pyFSaVbvJryX6W77Pe9MtmJb4PvLhLgh/TsxPLFSANcT67EC1t/qxjru5ZoADjKVEt2ejdx+xGvH49mcdfbc+l+L7gJtkH7GLKpE9pQNQWNHMAmj043PAXJZ++fiJObMRR2mye5VHEANzZWkZXMrXF7YjuG10S1pOU=" + }, + { + "sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e", + "sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214", + "conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd", + "nonce": "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54", + "plaintext": "🙈 🙉 🙊 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗", + "payload": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtUf05aMF89q1FLwJvaFJYICZoMYgRJHFLwPiOHce7fuAc40kX0wXJvipyBJ9HzCOj7CgtnC1/cmPCHR3s5AIORmroBWglm1LiFMohv1FSPEbaBD51VXxJa4JyWpYhreSOEjn1wd0lMKC9b+osV2N2tpbs+rbpQem2tRen3sWflmCqjkG5VOVwRErCuXuPb5+hYwd8BoZbfCrsiAVLd7YT44dRtKNBx6rkabWfddKSLtreHLDysOhQUVOp/XkE7OzSkWl6sky0Hva6qJJ/V726hMlomvcLHjE41iKmW2CpcZfOedg==" + } + ], + "encrypt_decrypt_long_msg": [ + { + "conversation_key": "8fc262099ce0d0bb9b89bac05bb9e04f9bc0090acc181fef6840ccee470371ed", + "nonce": "326bcb2c943cd6bb717588c9e5a7e738edf6ed14ec5f5344caa6ef56f0b9cff7", + "pattern": "x", + "repeat": 65535, + "plaintext_sha256": "09ab7495d3e61a76f0deb12cb0306f0696cbb17ffc12131368c7a939f12f56d3", + "payload_sha256": "90714492225faba06310bff2f249ebdc2a5e609d65a629f1c87f2d4ffc55330a" + }, + { + "conversation_key": "56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0", + "nonce": "ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0", + "pattern": "!", + "repeat": 65535, + "plaintext_sha256": "6af297793b72ae092c422e552c3bb3cbc310da274bd1cf9e31023a7fe4a2d75e", + "payload_sha256": "8013e45a109fad3362133132b460a2d5bce235fe71c8b8f4014793fb52a49844" + }, + { + "conversation_key": "7fc540779979e472bb8d12480b443d1e5eb1098eae546ef2390bee499bbf46be", + "nonce": "34905e82105c20de9a2f6cd385a0d541e6bcc10601d12481ff3a7575dc622033", + "pattern": "🦄", + "repeat": 16383, + "plaintext_sha256": "a249558d161b77297bc0cb311dde7d77190f6571b25c7e4429cd19044634a61f", + "payload_sha256": "b3348422471da1f3c59d79acfe2fe103f3cd24488109e5b18734cdb5953afd15" + }, + { + "conversation_key": "56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0", + "nonce": "ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0", + "pattern": "!", + "repeat": 65536, + "plaintext_sha256": "b007fe445ff5b583c095c4688c75d8afef66d4c93eb6aeebcea0942e670e1cd3", + "payload_sha256": "f816ffbcb053a0669488992e2d7c57c2d950d3d4af6e19a16297f3e565a4103f" + }, + { + "conversation_key": "56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0", + "nonce": "ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0", + "pattern": "a", + "repeat": 20000000, + "plaintext_sha256": "aded0ea9b4d06589b13d00bab483faf479d61ed5de21f1760aa7018a28e330e5", + "payload_sha256": "9e683311894d52e48a825837883c539263c7787c7fe024e1590d96776a31684b" + } + ] + }, + "invalid": { + "encrypt_msg_lengths": [0, 65536, 100000, 10000000], + "get_conversation_key": [ + { + "sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 higher than curve.n" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000000", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 is 0" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139", + "pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "note": "pub2 is invalid, no sqrt, all-ff" + }, + { + "sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "sec1 == curve.n" + }, + { + "sec1": "0000000000000000000000000000000000000000000000000000000000000002", + "pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "note": "pub2 is invalid, no sqrt" + }, + { + "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "pub2": "0000000000000000000000000000000000000000000000000000000000000000", + "note": "pub2 is point of order 3 on twist" + }, + { + "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "pub2": "eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d", + "note": "pub2 is point of order 13 on twist" + }, + { + "sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20", + "pub2": "709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f", + "note": "pub2 is point of order 3319 on twist" + } + ], + "decrypt": [ + { + "conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642", + "nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", + "plaintext": "n o b l e", + "payload": "#Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJdU0MIDf06CUvEvdnr1cp1fiMtlM/GrE92xAc1K5odTpCzUB+mjXgbaqtntBUbTToSUoT0ovrlPwzGjyp", + "note": "unknown encryption version" + }, + { + "conversation_key": "36f04e558af246352dcf73b692fbd3646a2207bd8abd4b1cd26b234db84d9481", + "nonce": "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781", + "plaintext": "⚠️", + "payload": "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBZFaha6EAIRueE9D1B1RuoiuFScC0Q94yjIuxZD3JStQtE8JMNacWFs9rlYP+ZydtHhRucp+lxfdvFlaGV/sQlqZz", + "note": "unknown encryption version 0" + }, + { + "conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642", + "nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db", + "plaintext": "n o s t r", + "payload": "Atфupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJZE0UICD06CUvEvdnr1cp1fiMtlM/GrE92xAc1EwsVCQEgWEu2gsHUVf4JAa3TpgkmFc3TWsax0v6n/Wq", + "note": "invalid base64" + }, + { + "conversation_key": "cff7bd6a3e29a450fd27f6c125d5edeb0987c475fd1e8d97591e0d4d8a89763c", + "nonce": "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25", + "plaintext": "¯\\_(ツ)_/¯", + "payload": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholyySBfeh+EN8wNB9gaLlg4j6wdBYh+3oK+mnxWu3NKRbSvQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "note": "invalid MAC" + }, + { + "conversation_key": "cfcc9cf682dfb00b11357f65bdc45e29156b69db424d20b3596919074f5bf957", + "nonce": "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4", + "plaintext": "🥎", + "payload": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0oRWQ8avl4OLqWZiTJ10vIgKrNqjoaX+fNhE9RqmR5g0f6BtUg1ijFMz71MO1D4lQLQfW7+UHva8PGYgQ1QpHlKgR", + "note": "invalid MAC" + }, + { + "conversation_key": "5254827d29177622d40a7b67cad014fe7137700c3c523903ebbe3e1b74d40214", + "nonce": "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1", + "plaintext": "elliptic-curve cryptography", + "payload": "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHB573MI7R7rrTYftpqmvUpahmBC2sngmI14/L0HjOZ7lWGJlzdh6luiOnGPc46cGxf08MRC4CIuxx3i2Lm0KqgJ7vA", + "note": "invalid padding" + }, + { + "conversation_key": "fea39aca9aa8340c3a78ae1f0902aa7e726946e4efcd7783379df8096029c496", + "nonce": "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330", + "plaintext": "noble", + "payload": "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwRrrPJFyAQYZh5VpjC2QYzny5LIQ9v9lhqmZR4WBYRNJ0ognHVNMwiFV1SHpvUFT8HHZN/m/QarflbvDHAtO6pY16", + "note": "invalid padding" + }, + { + "conversation_key": "0c4cffb7a6f7e706ec94b2e879f1fc54ff8de38d8db87e11787694d5392d5b3f", + "nonce": "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b", + "plaintext": "censorship-resistant and global social network", + "payload": "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6b2NZDaNm/TPkZGr75kbB6tCSoq7YRcbPiNfJXNch3Tf+o9+zZTMxwjgX/nm3yDKR2kHQMBhVleCB9uPuljl40AJ8kXRD0gjw+aYRJFUMK9gCETZAjjmrsCM+nGRZ1FfNsHr6Z", + "note": "invalid padding" + }, + { + "conversation_key": "5cd2d13b9e355aeb2452afbd3786870dbeecb9d355b12cb0a3b6e9da5744cd35", + "nonce": "b60036976a1ada277b948fd4caa065304b96964742b89d26f26a25263a5060bd", + "plaintext": "0", + "payload": "", + "note": "invalid payload length: 0" + }, + { + "conversation_key": "d61d3f09c7dfe1c0be91af7109b60a7d9d498920c90cbba1e137320fdd938853", + "nonce": "1a29d02c8b4527745a2ccb38bfa45655deb37bc338ab9289d756354cea1fd07c", + "plaintext": "1", + "payload": "Ag==", + "note": "invalid payload length: 4" + }, + { + "conversation_key": "873bb0fc665eb950a8e7d5971965539f6ebd645c83c08cd6a85aafbad0f0bc47", + "nonce": "c826d3c38e765ab8cc42060116cd1464b2a6ce01d33deba5dedfb48615306d4a", + "plaintext": "2", + "payload": "AqxgToSh3H7iLYRJjoWAM+vSv/Y1mgNlm6OWWjOYUClrFF8=", + "note": "invalid payload length: 48" + }, + { + "conversation_key": "9f2fef8f5401ac33f74641b568a7a30bb19409c76ffdc5eae2db6b39d2617fbe", + "nonce": "9ff6484642545221624eaac7b9ea27133a4cc2356682a6033aceeef043549861", + "plaintext": "3", + "payload": "Ap/2SEZCVFIhYk6qx7nqJxM6TMI1ZoKmAzrO7vBDVJhhuZXWiM20i/tIsbjT0KxkJs2MZjh1oXNYMO9ggfk7i47WQA==", + "note": "invalid payload length: 92" + } + ] + } + } +} \ No newline at end of file diff --git a/quartz/src/nativeInterop/libsodium/LICENSE b/quartz/src/nativeInterop/libsodium/LICENSE new file mode 100644 index 0000000000..95c22fbadf --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/LICENSE @@ -0,0 +1,18 @@ +/* + * ISC License + * + * Copyright (c) 2013-2026 + * Frank Denis + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/quartz/src/nativeInterop/libsodium/include/module.modulemap b/quartz/src/nativeInterop/libsodium/include/module.modulemap new file mode 100644 index 0000000000..d5e34df3c8 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/module.modulemap @@ -0,0 +1,4 @@ +module Clibsodium { + header "sodium.h" + export * +} diff --git a/quartz/src/nativeInterop/libsodium/include/sodium.h b/quartz/src/nativeInterop/libsodium/include/sodium.h new file mode 100644 index 0000000000..61a3168709 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium.h @@ -0,0 +1,81 @@ + +#ifndef sodium_H +#define sodium_H + +#include "sodium/version.h" + +#include "sodium/core.h" +#include "sodium/crypto_aead_aegis128l.h" +#include "sodium/crypto_aead_aegis256.h" +#include "sodium/crypto_aead_aes256gcm.h" +#include "sodium/crypto_aead_chacha20poly1305.h" +#include "sodium/crypto_aead_xchacha20poly1305.h" +#include "sodium/crypto_auth.h" +#include "sodium/crypto_auth_hmacsha256.h" +#include "sodium/crypto_auth_hmacsha512.h" +#include "sodium/crypto_auth_hmacsha512256.h" +#include "sodium/crypto_box.h" +#include "sodium/crypto_box_curve25519xsalsa20poly1305.h" +#include "sodium/crypto_core_hchacha20.h" +#include "sodium/crypto_core_hsalsa20.h" +#include "sodium/crypto_core_keccak1600.h" +#include "sodium/crypto_core_salsa20.h" +#include "sodium/crypto_core_salsa2012.h" +#include "sodium/crypto_core_salsa208.h" +#include "sodium/crypto_generichash.h" +#include "sodium/crypto_generichash_blake2b.h" +#include "sodium/crypto_hash.h" +#include "sodium/crypto_hash_sha256.h" +#include "sodium/crypto_hash_sha512.h" +#include "sodium/crypto_ipcrypt.h" +#include "sodium/crypto_kdf.h" +#include "sodium/crypto_kdf_hkdf_sha256.h" +#include "sodium/crypto_kdf_hkdf_sha512.h" +#include "sodium/crypto_kdf_blake2b.h" +#include "sodium/crypto_kdf_hkdf_sha256.h" +#include "sodium/crypto_kdf_hkdf_sha512.h" +#include "sodium/crypto_kx.h" +#include "sodium/crypto_onetimeauth.h" +#include "sodium/crypto_onetimeauth_poly1305.h" +#include "sodium/crypto_pwhash.h" +#include "sodium/crypto_pwhash_argon2i.h" +#include "sodium/crypto_scalarmult.h" +#include "sodium/crypto_scalarmult_curve25519.h" +#include "sodium/crypto_secretbox.h" +#include "sodium/crypto_secretbox_xsalsa20poly1305.h" +#include "sodium/crypto_secretstream_xchacha20poly1305.h" +#include "sodium/crypto_shorthash.h" +#include "sodium/crypto_shorthash_siphash24.h" +#include "sodium/crypto_sign.h" +#include "sodium/crypto_sign_ed25519.h" +#include "sodium/crypto_stream.h" +#include "sodium/crypto_stream_chacha20.h" +#include "sodium/crypto_stream_salsa20.h" +#include "sodium/crypto_stream_xsalsa20.h" +#include "sodium/crypto_verify_16.h" +#include "sodium/crypto_verify_32.h" +#include "sodium/crypto_verify_64.h" +#include "sodium/crypto_xof_shake128.h" +#include "sodium/crypto_xof_shake256.h" +#include "sodium/crypto_xof_turboshake128.h" +#include "sodium/crypto_xof_turboshake256.h" +#include "sodium/randombytes.h" +#include "sodium/randombytes_internal_random.h" +#include "sodium/randombytes_sysrandom.h" +#include "sodium/runtime.h" +#include "sodium/utils.h" + +#ifndef SODIUM_LIBRARY_MINIMAL +#include "sodium/crypto_box_curve25519xchacha20poly1305.h" +#include "sodium/crypto_core_ed25519.h" +#include "sodium/crypto_core_ristretto255.h" +#include "sodium/crypto_pwhash_scryptsalsa208sha256.h" +#include "sodium/crypto_scalarmult_ed25519.h" +#include "sodium/crypto_scalarmult_ristretto255.h" +#include "sodium/crypto_secretbox_xchacha20poly1305.h" +#include "sodium/crypto_stream_salsa2012.h" +#include "sodium/crypto_stream_salsa208.h" +#include "sodium/crypto_stream_xchacha20.h" +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/core.h b/quartz/src/nativeInterop/libsodium/include/sodium/core.h new file mode 100644 index 0000000000..dd088d2cae --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/core.h @@ -0,0 +1,28 @@ + +#ifndef sodium_core_H +#define sodium_core_H + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +SODIUM_EXPORT +int sodium_init(void) + __attribute__ ((warn_unused_result)); + +/* ---- */ + +SODIUM_EXPORT +int sodium_set_misuse_handler(void (*handler)(void)); + +SODIUM_EXPORT +void sodium_misuse(void) + __attribute__ ((noreturn)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis128l.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis128l.h new file mode 100644 index 0000000000..0ad019fc94 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis128l.h @@ -0,0 +1,92 @@ +#ifndef crypto_aead_aegis128l_H +#define crypto_aead_aegis128l_H + +#include + +#include "export.h" + +#ifdef __cplusplus +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wlong-long" +#endif +extern "C" { +#endif + +#define crypto_aead_aegis128l_KEYBYTES 16U +SODIUM_EXPORT +size_t crypto_aead_aegis128l_keybytes(void); + +#define crypto_aead_aegis128l_NSECBYTES 0U +SODIUM_EXPORT +size_t crypto_aead_aegis128l_nsecbytes(void); + +#define crypto_aead_aegis128l_NPUBBYTES 16U +SODIUM_EXPORT +size_t crypto_aead_aegis128l_npubbytes(void); + +#define crypto_aead_aegis128l_ABYTES 32U +SODIUM_EXPORT +size_t crypto_aead_aegis128l_abytes(void); + +#define crypto_aead_aegis128l_MESSAGEBYTES_MAX \ + SODIUM_MIN(SODIUM_SIZE_MAX - crypto_aead_aegis128l_ABYTES, (1ULL << 61) - 1) +SODIUM_EXPORT +size_t crypto_aead_aegis128l_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_aead_aegis128l_encrypt(unsigned char *c, + unsigned long long *clen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) __attribute__((nonnull(1, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_aegis128l_decrypt(unsigned char *m, + unsigned long long *mlen_p, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) __attribute__((warn_unused_result)) +__attribute__((nonnull(4, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_aegis128l_encrypt_detached(unsigned char *c, + unsigned char *mac, + unsigned long long *maclen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__((nonnull(1, 2, 9, 10))); + +SODIUM_EXPORT +int crypto_aead_aegis128l_decrypt_detached(unsigned char *m, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *mac, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__((warn_unused_result)) __attribute__((nonnull(3, 5, 8, 9))); + +SODIUM_EXPORT +void crypto_aead_aegis128l_keygen(unsigned char k[crypto_aead_aegis128l_KEYBYTES]) + __attribute__((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis256.h new file mode 100644 index 0000000000..26bd18ace1 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis256.h @@ -0,0 +1,92 @@ +#ifndef crypto_aead_aegis256_H +#define crypto_aead_aegis256_H + +#include + +#include "export.h" + +#ifdef __cplusplus +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wlong-long" +#endif +extern "C" { +#endif + +#define crypto_aead_aegis256_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_aead_aegis256_keybytes(void); + +#define crypto_aead_aegis256_NSECBYTES 0U +SODIUM_EXPORT +size_t crypto_aead_aegis256_nsecbytes(void); + +#define crypto_aead_aegis256_NPUBBYTES 32U +SODIUM_EXPORT +size_t crypto_aead_aegis256_npubbytes(void); + +#define crypto_aead_aegis256_ABYTES 32U +SODIUM_EXPORT +size_t crypto_aead_aegis256_abytes(void); + +#define crypto_aead_aegis256_MESSAGEBYTES_MAX \ + SODIUM_MIN(SODIUM_SIZE_MAX - crypto_aead_aegis256_ABYTES, (1ULL << 61) - 1) +SODIUM_EXPORT +size_t crypto_aead_aegis256_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_aead_aegis256_encrypt(unsigned char *c, + unsigned long long *clen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) __attribute__((nonnull(1, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_aegis256_decrypt(unsigned char *m, + unsigned long long *mlen_p, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) __attribute__((warn_unused_result)) +__attribute__((nonnull(4, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_aegis256_encrypt_detached(unsigned char *c, + unsigned char *mac, + unsigned long long *maclen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__((nonnull(1, 2, 9, 10))); + +SODIUM_EXPORT +int crypto_aead_aegis256_decrypt_detached(unsigned char *m, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *mac, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__((warn_unused_result)) __attribute__((nonnull(3, 5, 8, 9))); + +SODIUM_EXPORT +void crypto_aead_aegis256_keygen(unsigned char k[crypto_aead_aegis256_KEYBYTES]) + __attribute__((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aes256gcm.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aes256gcm.h new file mode 100644 index 0000000000..9baeb3f19f --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aes256gcm.h @@ -0,0 +1,179 @@ +#ifndef crypto_aead_aes256gcm_H +#define crypto_aead_aes256gcm_H + +/* + * WARNING: Despite being the most popular AEAD construction due to its + * use in TLS, safely using AES-GCM in a different context is tricky. + * + * No more than ~ 350 GB of input data should be encrypted with a given key. + * This is for ~ 16 KB messages -- Actual figures vary according to + * message sizes. + * + * In addition, nonces are short and repeated nonces would totally destroy + * the security of this scheme. + * + * Nonces should thus come from atomic counters, which can be difficult to + * set up in a distributed environment. + * + * Unless you absolutely need AES-GCM, use crypto_aead_xchacha20poly1305_ietf_*() + * instead. It doesn't have any of these limitations. + * Or, if you don't need to authenticate additional data, just stick to + * crypto_secretbox(). + */ + +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +SODIUM_EXPORT +int crypto_aead_aes256gcm_is_available(void); + +#define crypto_aead_aes256gcm_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_aead_aes256gcm_keybytes(void); + +#define crypto_aead_aes256gcm_NSECBYTES 0U +SODIUM_EXPORT +size_t crypto_aead_aes256gcm_nsecbytes(void); + +#define crypto_aead_aes256gcm_NPUBBYTES 12U +SODIUM_EXPORT +size_t crypto_aead_aes256gcm_npubbytes(void); + +#define crypto_aead_aes256gcm_ABYTES 16U +SODIUM_EXPORT +size_t crypto_aead_aes256gcm_abytes(void); + +#define crypto_aead_aes256gcm_MESSAGEBYTES_MAX \ + SODIUM_MIN(SODIUM_SIZE_MAX - crypto_aead_aes256gcm_ABYTES, \ + (16ULL * ((1ULL << 32) - 2ULL))) +SODIUM_EXPORT +size_t crypto_aead_aes256gcm_messagebytes_max(void); + +typedef struct CRYPTO_ALIGN(16) crypto_aead_aes256gcm_state_ { + unsigned char opaque[512]; +} crypto_aead_aes256gcm_state; + +SODIUM_EXPORT +size_t crypto_aead_aes256gcm_statebytes(void); + +SODIUM_EXPORT +int crypto_aead_aes256gcm_encrypt(unsigned char *c, + unsigned long long *clen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((nonnull(1, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_aes256gcm_decrypt(unsigned char *m, + unsigned long long *mlen_p, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_aes256gcm_encrypt_detached(unsigned char *c, + unsigned char *mac, + unsigned long long *maclen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((nonnull(1, 2, 9, 10))); + +SODIUM_EXPORT +int crypto_aead_aes256gcm_decrypt_detached(unsigned char *m, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *mac, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); + +/* -- Precomputation interface -- */ + +SODIUM_EXPORT +int crypto_aead_aes256gcm_beforenm(crypto_aead_aes256gcm_state *ctx_, + const unsigned char *k) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_aead_aes256gcm_encrypt_afternm(unsigned char *c, + unsigned long long *clen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const crypto_aead_aes256gcm_state *ctx_) + __attribute__ ((nonnull(1, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_aes256gcm_decrypt_afternm(unsigned char *m, + unsigned long long *mlen_p, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const crypto_aead_aes256gcm_state *ctx_) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_aes256gcm_encrypt_detached_afternm(unsigned char *c, + unsigned char *mac, + unsigned long long *maclen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const crypto_aead_aes256gcm_state *ctx_) + __attribute__ ((nonnull(1, 2, 9, 10))); + +SODIUM_EXPORT +int crypto_aead_aes256gcm_decrypt_detached_afternm(unsigned char *m, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *mac, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const crypto_aead_aes256gcm_state *ctx_) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); + +SODIUM_EXPORT +void crypto_aead_aes256gcm_keygen(unsigned char k[crypto_aead_aes256gcm_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_chacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_chacha20poly1305.h new file mode 100644 index 0000000000..5d671df142 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_chacha20poly1305.h @@ -0,0 +1,180 @@ +#ifndef crypto_aead_chacha20poly1305_H +#define crypto_aead_chacha20poly1305_H + +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +/* -- IETF ChaCha20-Poly1305 construction with a 96-bit nonce and a 32-bit internal counter -- */ + +#define crypto_aead_chacha20poly1305_ietf_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_ietf_keybytes(void); + +#define crypto_aead_chacha20poly1305_ietf_NSECBYTES 0U +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_ietf_nsecbytes(void); + +#define crypto_aead_chacha20poly1305_ietf_NPUBBYTES 12U + +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_ietf_npubbytes(void); + +#define crypto_aead_chacha20poly1305_ietf_ABYTES 16U +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_ietf_abytes(void); + +#define crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX \ + SODIUM_MIN(SODIUM_SIZE_MAX - crypto_aead_chacha20poly1305_ietf_ABYTES, \ + (64ULL * ((1ULL << 32) - 1ULL))) +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_ietf_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_aead_chacha20poly1305_ietf_encrypt(unsigned char *c, + unsigned long long *clen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((nonnull(1, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_chacha20poly1305_ietf_decrypt(unsigned char *m, + unsigned long long *mlen_p, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_chacha20poly1305_ietf_encrypt_detached(unsigned char *c, + unsigned char *mac, + unsigned long long *maclen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((nonnull(1, 2, 9, 10))); + +SODIUM_EXPORT +int crypto_aead_chacha20poly1305_ietf_decrypt_detached(unsigned char *m, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *mac, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); + +SODIUM_EXPORT +void crypto_aead_chacha20poly1305_ietf_keygen(unsigned char k[crypto_aead_chacha20poly1305_ietf_KEYBYTES]) + __attribute__ ((nonnull)); + +/* -- Original ChaCha20-Poly1305 construction with a 64-bit nonce and a 64-bit internal counter -- */ + +#define crypto_aead_chacha20poly1305_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_keybytes(void); + +#define crypto_aead_chacha20poly1305_NSECBYTES 0U +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_nsecbytes(void); + +#define crypto_aead_chacha20poly1305_NPUBBYTES 8U +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_npubbytes(void); + +#define crypto_aead_chacha20poly1305_ABYTES 16U +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_abytes(void); + +#define crypto_aead_chacha20poly1305_MESSAGEBYTES_MAX \ + (SODIUM_SIZE_MAX - crypto_aead_chacha20poly1305_ABYTES) +SODIUM_EXPORT +size_t crypto_aead_chacha20poly1305_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_aead_chacha20poly1305_encrypt(unsigned char *c, + unsigned long long *clen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((nonnull(1, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_chacha20poly1305_decrypt(unsigned char *m, + unsigned long long *mlen_p, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_chacha20poly1305_encrypt_detached(unsigned char *c, + unsigned char *mac, + unsigned long long *maclen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((nonnull(1, 2, 9, 10))); + +SODIUM_EXPORT +int crypto_aead_chacha20poly1305_decrypt_detached(unsigned char *m, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *mac, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); + +SODIUM_EXPORT +void crypto_aead_chacha20poly1305_keygen(unsigned char k[crypto_aead_chacha20poly1305_KEYBYTES]) + __attribute__ ((nonnull)); + +/* Aliases */ + +#define crypto_aead_chacha20poly1305_IETF_KEYBYTES crypto_aead_chacha20poly1305_ietf_KEYBYTES +#define crypto_aead_chacha20poly1305_IETF_NSECBYTES crypto_aead_chacha20poly1305_ietf_NSECBYTES +#define crypto_aead_chacha20poly1305_IETF_NPUBBYTES crypto_aead_chacha20poly1305_ietf_NPUBBYTES +#define crypto_aead_chacha20poly1305_IETF_ABYTES crypto_aead_chacha20poly1305_ietf_ABYTES +#define crypto_aead_chacha20poly1305_IETF_MESSAGEBYTES_MAX crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_xchacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_xchacha20poly1305.h new file mode 100644 index 0000000000..6643b0cbf5 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_xchacha20poly1305.h @@ -0,0 +1,100 @@ +#ifndef crypto_aead_xchacha20poly1305_H +#define crypto_aead_xchacha20poly1305_H + +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_aead_xchacha20poly1305_ietf_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_aead_xchacha20poly1305_ietf_keybytes(void); + +#define crypto_aead_xchacha20poly1305_ietf_NSECBYTES 0U +SODIUM_EXPORT +size_t crypto_aead_xchacha20poly1305_ietf_nsecbytes(void); + +#define crypto_aead_xchacha20poly1305_ietf_NPUBBYTES 24U +SODIUM_EXPORT +size_t crypto_aead_xchacha20poly1305_ietf_npubbytes(void); + +#define crypto_aead_xchacha20poly1305_ietf_ABYTES 16U +SODIUM_EXPORT +size_t crypto_aead_xchacha20poly1305_ietf_abytes(void); + +#define crypto_aead_xchacha20poly1305_ietf_MESSAGEBYTES_MAX \ + (SODIUM_SIZE_MAX - crypto_aead_xchacha20poly1305_ietf_ABYTES) +SODIUM_EXPORT +size_t crypto_aead_xchacha20poly1305_ietf_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_aead_xchacha20poly1305_ietf_encrypt(unsigned char *c, + unsigned long long *clen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((nonnull(1, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_xchacha20poly1305_ietf_decrypt(unsigned char *m, + unsigned long long *mlen_p, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); + +SODIUM_EXPORT +int crypto_aead_xchacha20poly1305_ietf_encrypt_detached(unsigned char *c, + unsigned char *mac, + unsigned long long *maclen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *nsec, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((nonnull(1, 2, 9, 10))); + +SODIUM_EXPORT +int crypto_aead_xchacha20poly1305_ietf_decrypt_detached(unsigned char *m, + unsigned char *nsec, + const unsigned char *c, + unsigned long long clen, + const unsigned char *mac, + const unsigned char *ad, + unsigned long long adlen, + const unsigned char *npub, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); + +SODIUM_EXPORT +void crypto_aead_xchacha20poly1305_ietf_keygen(unsigned char k[crypto_aead_xchacha20poly1305_ietf_KEYBYTES]) + __attribute__ ((nonnull)); + +/* Aliases */ + +#define crypto_aead_xchacha20poly1305_IETF_KEYBYTES crypto_aead_xchacha20poly1305_ietf_KEYBYTES +#define crypto_aead_xchacha20poly1305_IETF_NSECBYTES crypto_aead_xchacha20poly1305_ietf_NSECBYTES +#define crypto_aead_xchacha20poly1305_IETF_NPUBBYTES crypto_aead_xchacha20poly1305_ietf_NPUBBYTES +#define crypto_aead_xchacha20poly1305_IETF_ABYTES crypto_aead_xchacha20poly1305_ietf_ABYTES +#define crypto_aead_xchacha20poly1305_IETF_MESSAGEBYTES_MAX crypto_aead_xchacha20poly1305_ietf_MESSAGEBYTES_MAX + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth.h new file mode 100644 index 0000000000..540aee0e8d --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth.h @@ -0,0 +1,46 @@ +#ifndef crypto_auth_H +#define crypto_auth_H + +#include + +#include "crypto_auth_hmacsha512256.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_auth_BYTES crypto_auth_hmacsha512256_BYTES +SODIUM_EXPORT +size_t crypto_auth_bytes(void); + +#define crypto_auth_KEYBYTES crypto_auth_hmacsha512256_KEYBYTES +SODIUM_EXPORT +size_t crypto_auth_keybytes(void); + +#define crypto_auth_PRIMITIVE "hmacsha512256" +SODIUM_EXPORT +const char *crypto_auth_primitive(void); + +SODIUM_EXPORT +int crypto_auth(unsigned char *out, const unsigned char *in, + unsigned long long inlen, const unsigned char *k) + __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_auth_verify(const unsigned char *h, const unsigned char *in, + unsigned long long inlen, const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +void crypto_auth_keygen(unsigned char k[crypto_auth_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha256.h new file mode 100644 index 0000000000..3da864c7d2 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha256.h @@ -0,0 +1,70 @@ +#ifndef crypto_auth_hmacsha256_H +#define crypto_auth_hmacsha256_H + +#include +#include "crypto_hash_sha256.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_auth_hmacsha256_BYTES 32U +SODIUM_EXPORT +size_t crypto_auth_hmacsha256_bytes(void); + +#define crypto_auth_hmacsha256_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_auth_hmacsha256_keybytes(void); + +SODIUM_EXPORT +int crypto_auth_hmacsha256(unsigned char *out, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *k) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_auth_hmacsha256_verify(const unsigned char *h, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +/* ------------------------------------------------------------------------- */ + +typedef struct crypto_auth_hmacsha256_state { + crypto_hash_sha256_state ictx; + crypto_hash_sha256_state octx; +} crypto_auth_hmacsha256_state; + +SODIUM_EXPORT +size_t crypto_auth_hmacsha256_statebytes(void); + +SODIUM_EXPORT +int crypto_auth_hmacsha256_init(crypto_auth_hmacsha256_state *state, + const unsigned char *key, + size_t keylen) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_auth_hmacsha256_update(crypto_auth_hmacsha256_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_auth_hmacsha256_final(crypto_auth_hmacsha256_state *state, + unsigned char *out) __attribute__ ((nonnull)); + + +SODIUM_EXPORT +void crypto_auth_hmacsha256_keygen(unsigned char k[crypto_auth_hmacsha256_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512.h new file mode 100644 index 0000000000..d992cb8163 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512.h @@ -0,0 +1,68 @@ +#ifndef crypto_auth_hmacsha512_H +#define crypto_auth_hmacsha512_H + +#include +#include "crypto_hash_sha512.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_auth_hmacsha512_BYTES 64U +SODIUM_EXPORT +size_t crypto_auth_hmacsha512_bytes(void); + +#define crypto_auth_hmacsha512_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_auth_hmacsha512_keybytes(void); + +SODIUM_EXPORT +int crypto_auth_hmacsha512(unsigned char *out, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *k) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_auth_hmacsha512_verify(const unsigned char *h, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +/* ------------------------------------------------------------------------- */ + +typedef struct crypto_auth_hmacsha512_state { + crypto_hash_sha512_state ictx; + crypto_hash_sha512_state octx; +} crypto_auth_hmacsha512_state; + +SODIUM_EXPORT +size_t crypto_auth_hmacsha512_statebytes(void); + +SODIUM_EXPORT +int crypto_auth_hmacsha512_init(crypto_auth_hmacsha512_state *state, + const unsigned char *key, + size_t keylen) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_auth_hmacsha512_update(crypto_auth_hmacsha512_state *state, + const unsigned char *in, + unsigned long long inlen) __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_auth_hmacsha512_final(crypto_auth_hmacsha512_state *state, + unsigned char *out) __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_auth_hmacsha512_keygen(unsigned char k[crypto_auth_hmacsha512_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512256.h new file mode 100644 index 0000000000..3fb5263892 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512256.h @@ -0,0 +1,65 @@ +#ifndef crypto_auth_hmacsha512256_H +#define crypto_auth_hmacsha512256_H + +#include +#include "crypto_auth_hmacsha512.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_auth_hmacsha512256_BYTES 32U +SODIUM_EXPORT +size_t crypto_auth_hmacsha512256_bytes(void); + +#define crypto_auth_hmacsha512256_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_auth_hmacsha512256_keybytes(void); + +SODIUM_EXPORT +int crypto_auth_hmacsha512256(unsigned char *out, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *k) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_auth_hmacsha512256_verify(const unsigned char *h, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +/* ------------------------------------------------------------------------- */ + +typedef crypto_auth_hmacsha512_state crypto_auth_hmacsha512256_state; + +SODIUM_EXPORT +size_t crypto_auth_hmacsha512256_statebytes(void); + +SODIUM_EXPORT +int crypto_auth_hmacsha512256_init(crypto_auth_hmacsha512256_state *state, + const unsigned char *key, + size_t keylen) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_auth_hmacsha512256_update(crypto_auth_hmacsha512256_state *state, + const unsigned char *in, + unsigned long long inlen) __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_auth_hmacsha512256_final(crypto_auth_hmacsha512256_state *state, + unsigned char *out) __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_auth_hmacsha512256_keygen(unsigned char k[crypto_auth_hmacsha512256_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box.h new file mode 100644 index 0000000000..e060dd29fc --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box.h @@ -0,0 +1,177 @@ +#ifndef crypto_box_H +#define crypto_box_H + +/* + * THREAD SAFETY: crypto_box_keypair() is thread-safe, + * provided that sodium_init() was called before. + * + * Other functions are always thread-safe. + */ + +#include + +#include "crypto_box_curve25519xsalsa20poly1305.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_box_SEEDBYTES crypto_box_curve25519xsalsa20poly1305_SEEDBYTES +SODIUM_EXPORT +size_t crypto_box_seedbytes(void); + +#define crypto_box_PUBLICKEYBYTES crypto_box_curve25519xsalsa20poly1305_PUBLICKEYBYTES +SODIUM_EXPORT +size_t crypto_box_publickeybytes(void); + +#define crypto_box_SECRETKEYBYTES crypto_box_curve25519xsalsa20poly1305_SECRETKEYBYTES +SODIUM_EXPORT +size_t crypto_box_secretkeybytes(void); + +#define crypto_box_NONCEBYTES crypto_box_curve25519xsalsa20poly1305_NONCEBYTES +SODIUM_EXPORT +size_t crypto_box_noncebytes(void); + +#define crypto_box_MACBYTES crypto_box_curve25519xsalsa20poly1305_MACBYTES +SODIUM_EXPORT +size_t crypto_box_macbytes(void); + +#define crypto_box_MESSAGEBYTES_MAX crypto_box_curve25519xsalsa20poly1305_MESSAGEBYTES_MAX +SODIUM_EXPORT +size_t crypto_box_messagebytes_max(void); + +#define crypto_box_PRIMITIVE "curve25519xsalsa20poly1305" +SODIUM_EXPORT +const char *crypto_box_primitive(void); + +SODIUM_EXPORT +int crypto_box_seed_keypair(unsigned char *pk, unsigned char *sk, + const unsigned char *seed) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_box_keypair(unsigned char *pk, unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_box_easy(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *pk, const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4, 5, 6))); + +SODIUM_EXPORT +int crypto_box_open_easy(unsigned char *m, const unsigned char *c, + unsigned long long clen, const unsigned char *n, + const unsigned char *pk, const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5, 6))); + +SODIUM_EXPORT +int crypto_box_detached(unsigned char *c, unsigned char *mac, + const unsigned char *m, unsigned long long mlen, + const unsigned char *n, const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 2, 5, 6, 7))); + +SODIUM_EXPORT +int crypto_box_open_detached(unsigned char *m, const unsigned char *c, + const unsigned char *mac, + unsigned long long clen, + const unsigned char *n, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6, 7))); + +/* -- Precomputation interface -- */ + +#define crypto_box_BEFORENMBYTES crypto_box_curve25519xsalsa20poly1305_BEFORENMBYTES +SODIUM_EXPORT +size_t crypto_box_beforenmbytes(void); + +SODIUM_EXPORT +int crypto_box_beforenm(unsigned char *k, const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_box_easy_afternm(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_box_open_easy_afternm(unsigned char *m, const unsigned char *c, + unsigned long long clen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +SODIUM_EXPORT +int crypto_box_detached_afternm(unsigned char *c, unsigned char *mac, + const unsigned char *m, unsigned long long mlen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((nonnull(1, 2, 5, 6))); + +SODIUM_EXPORT +int crypto_box_open_detached_afternm(unsigned char *m, const unsigned char *c, + const unsigned char *mac, + unsigned long long clen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6))); + +/* -- Ephemeral SK interface -- */ + +#define crypto_box_SEALBYTES (crypto_box_PUBLICKEYBYTES + crypto_box_MACBYTES) +SODIUM_EXPORT +size_t crypto_box_sealbytes(void); + +SODIUM_EXPORT +int crypto_box_seal(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *pk) + __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_box_seal_open(unsigned char *m, const unsigned char *c, + unsigned long long clen, + const unsigned char *pk, const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +/* -- NaCl compatibility interface ; Requires padding -- */ + +#define crypto_box_ZEROBYTES crypto_box_curve25519xsalsa20poly1305_ZEROBYTES +SODIUM_EXPORT +size_t crypto_box_zerobytes(void); + +#define crypto_box_BOXZEROBYTES crypto_box_curve25519xsalsa20poly1305_BOXZEROBYTES +SODIUM_EXPORT +size_t crypto_box_boxzerobytes(void); + +SODIUM_EXPORT +int crypto_box(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *pk, const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4, 5, 6))); + +SODIUM_EXPORT +int crypto_box_open(unsigned char *m, const unsigned char *c, + unsigned long long clen, const unsigned char *n, + const unsigned char *pk, const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5, 6))); + +SODIUM_EXPORT +int crypto_box_afternm(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_box_open_afternm(unsigned char *m, const unsigned char *c, + unsigned long long clen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xchacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xchacha20poly1305.h new file mode 100644 index 0000000000..26a3d31efa --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xchacha20poly1305.h @@ -0,0 +1,164 @@ + +#ifndef crypto_box_curve25519xchacha20poly1305_H +#define crypto_box_curve25519xchacha20poly1305_H + +#include +#include "crypto_stream_xchacha20.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_box_curve25519xchacha20poly1305_SEEDBYTES 32U +SODIUM_EXPORT +size_t crypto_box_curve25519xchacha20poly1305_seedbytes(void); + +#define crypto_box_curve25519xchacha20poly1305_PUBLICKEYBYTES 32U +SODIUM_EXPORT +size_t crypto_box_curve25519xchacha20poly1305_publickeybytes(void); + +#define crypto_box_curve25519xchacha20poly1305_SECRETKEYBYTES 32U +SODIUM_EXPORT +size_t crypto_box_curve25519xchacha20poly1305_secretkeybytes(void); + +#define crypto_box_curve25519xchacha20poly1305_BEFORENMBYTES 32U +SODIUM_EXPORT +size_t crypto_box_curve25519xchacha20poly1305_beforenmbytes(void); + +#define crypto_box_curve25519xchacha20poly1305_NONCEBYTES 24U +SODIUM_EXPORT +size_t crypto_box_curve25519xchacha20poly1305_noncebytes(void); + +#define crypto_box_curve25519xchacha20poly1305_MACBYTES 16U +SODIUM_EXPORT +size_t crypto_box_curve25519xchacha20poly1305_macbytes(void); + +#define crypto_box_curve25519xchacha20poly1305_MESSAGEBYTES_MAX \ + (crypto_stream_xchacha20_MESSAGEBYTES_MAX - crypto_box_curve25519xchacha20poly1305_MACBYTES) +SODIUM_EXPORT +size_t crypto_box_curve25519xchacha20poly1305_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_seed_keypair(unsigned char *pk, + unsigned char *sk, + const unsigned char *seed) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_keypair(unsigned char *pk, + unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_easy(unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4, 5, 6))); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_open_easy(unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *n, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5, 6))); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_detached(unsigned char *c, + unsigned char *mac, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 2, 5, 6, 7))); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_open_detached(unsigned char *m, + const unsigned char *c, + const unsigned char *mac, + unsigned long long clen, + const unsigned char *n, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6, 7))); + +/* -- Precomputation interface -- */ + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_beforenm(unsigned char *k, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_easy_afternm(unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_open_easy_afternm(unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_detached_afternm(unsigned char *c, + unsigned char *mac, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 2, 5, 6))); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_open_detached_afternm(unsigned char *m, + const unsigned char *c, + const unsigned char *mac, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6))); + +/* -- Ephemeral SK interface -- */ + +#define crypto_box_curve25519xchacha20poly1305_SEALBYTES \ + (crypto_box_curve25519xchacha20poly1305_PUBLICKEYBYTES + \ + crypto_box_curve25519xchacha20poly1305_MACBYTES) + +SODIUM_EXPORT +size_t crypto_box_curve25519xchacha20poly1305_sealbytes(void); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_seal(unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk) + __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_box_curve25519xchacha20poly1305_seal_open(unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xsalsa20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xsalsa20poly1305.h new file mode 100644 index 0000000000..e733f49995 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xsalsa20poly1305.h @@ -0,0 +1,112 @@ +#ifndef crypto_box_curve25519xsalsa20poly1305_H +#define crypto_box_curve25519xsalsa20poly1305_H + +#include +#include "crypto_stream_xsalsa20.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_box_curve25519xsalsa20poly1305_SEEDBYTES 32U +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_seedbytes(void); + +#define crypto_box_curve25519xsalsa20poly1305_PUBLICKEYBYTES 32U +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_publickeybytes(void); + +#define crypto_box_curve25519xsalsa20poly1305_SECRETKEYBYTES 32U +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_secretkeybytes(void); + +#define crypto_box_curve25519xsalsa20poly1305_BEFORENMBYTES 32U +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_beforenmbytes(void); + +#define crypto_box_curve25519xsalsa20poly1305_NONCEBYTES 24U +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_noncebytes(void); + +#define crypto_box_curve25519xsalsa20poly1305_MACBYTES 16U +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_macbytes(void); + +/* Only for the libsodium API - The NaCl compatibility API would require BOXZEROBYTES extra bytes */ +#define crypto_box_curve25519xsalsa20poly1305_MESSAGEBYTES_MAX \ + (crypto_stream_xsalsa20_MESSAGEBYTES_MAX - crypto_box_curve25519xsalsa20poly1305_MACBYTES) +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_box_curve25519xsalsa20poly1305_seed_keypair(unsigned char *pk, + unsigned char *sk, + const unsigned char *seed) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_box_curve25519xsalsa20poly1305_keypair(unsigned char *pk, + unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_box_curve25519xsalsa20poly1305_beforenm(unsigned char *k, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +/* -- NaCl compatibility interface ; Requires padding -- */ + +#define crypto_box_curve25519xsalsa20poly1305_BOXZEROBYTES 16U +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_boxzerobytes(void); + +#define crypto_box_curve25519xsalsa20poly1305_ZEROBYTES \ + (crypto_box_curve25519xsalsa20poly1305_BOXZEROBYTES + \ + crypto_box_curve25519xsalsa20poly1305_MACBYTES) +SODIUM_EXPORT +size_t crypto_box_curve25519xsalsa20poly1305_zerobytes(void); + +SODIUM_EXPORT +int crypto_box_curve25519xsalsa20poly1305(unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4, 5, 6))); + +SODIUM_EXPORT +int crypto_box_curve25519xsalsa20poly1305_open(unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *n, + const unsigned char *pk, + const unsigned char *sk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5, 6))); + +SODIUM_EXPORT +int crypto_box_curve25519xsalsa20poly1305_afternm(unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_box_curve25519xsalsa20poly1305_open_afternm(unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ed25519.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ed25519.h new file mode 100644 index 0000000000..8f2eeee936 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ed25519.h @@ -0,0 +1,104 @@ +#ifndef crypto_core_ed25519_H +#define crypto_core_ed25519_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_core_ed25519_BYTES 32 +SODIUM_EXPORT +size_t crypto_core_ed25519_bytes(void); + +#define crypto_core_ed25519_UNIFORMBYTES 32 +SODIUM_EXPORT +size_t crypto_core_ed25519_uniformbytes(void); + +#define crypto_core_ed25519_HASHBYTES 64 +SODIUM_EXPORT +size_t crypto_core_ed25519_hashbytes(void); + +#define crypto_core_ed25519_SCALARBYTES 32 +SODIUM_EXPORT +size_t crypto_core_ed25519_scalarbytes(void); + +#define crypto_core_ed25519_NONREDUCEDSCALARBYTES 64 +SODIUM_EXPORT +size_t crypto_core_ed25519_nonreducedscalarbytes(void); + +SODIUM_EXPORT +int crypto_core_ed25519_is_valid_point(const unsigned char *p) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_core_ed25519_add(unsigned char *r, + const unsigned char *p, const unsigned char *q) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_core_ed25519_sub(unsigned char *r, + const unsigned char *p, const unsigned char *q) + __attribute__ ((nonnull)); + +/* + * Removal notice: this function is undocumented and has been marked as + * deprecated since 2019. It will be removed. Bindings should not refer to it. + */ +SODIUM_EXPORT +int crypto_core_ed25519_from_uniform(unsigned char *p, const unsigned char *r) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_core_ed25519_from_hash(unsigned char *p, const unsigned char *h) + __attribute__ ((nonnull)) __attribute__ ((deprecated)); + +SODIUM_EXPORT +void crypto_core_ed25519_random(unsigned char *p) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ed25519_scalar_random(unsigned char *r) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_core_ed25519_scalar_invert(unsigned char *recip, const unsigned char *s) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ed25519_scalar_negate(unsigned char *neg, const unsigned char *s) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ed25519_scalar_complement(unsigned char *comp, const unsigned char *s) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ed25519_scalar_add(unsigned char *z, const unsigned char *x, + const unsigned char *y) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ed25519_scalar_sub(unsigned char *z, const unsigned char *x, + const unsigned char *y) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ed25519_scalar_mul(unsigned char *z, const unsigned char *x, + const unsigned char *y) + __attribute__ ((nonnull)); + +/* + * The interval `s` is sampled from should be at least 317 bits to ensure almost + * uniformity of `r` over `L`. + */ +SODIUM_EXPORT +void crypto_core_ed25519_scalar_reduce(unsigned char *r, const unsigned char *s) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hchacha20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hchacha20.h new file mode 100644 index 0000000000..ece141b09b --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hchacha20.h @@ -0,0 +1,36 @@ +#ifndef crypto_core_hchacha20_H +#define crypto_core_hchacha20_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_core_hchacha20_OUTPUTBYTES 32U +SODIUM_EXPORT +size_t crypto_core_hchacha20_outputbytes(void); + +#define crypto_core_hchacha20_INPUTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_hchacha20_inputbytes(void); + +#define crypto_core_hchacha20_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_core_hchacha20_keybytes(void); + +#define crypto_core_hchacha20_CONSTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_hchacha20_constbytes(void); + +SODIUM_EXPORT +int crypto_core_hchacha20(unsigned char *out, const unsigned char *in, + const unsigned char *k, const unsigned char *c) + __attribute__ ((nonnull(1, 2, 3))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hsalsa20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hsalsa20.h new file mode 100644 index 0000000000..4bf7a48786 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hsalsa20.h @@ -0,0 +1,36 @@ +#ifndef crypto_core_hsalsa20_H +#define crypto_core_hsalsa20_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_core_hsalsa20_OUTPUTBYTES 32U +SODIUM_EXPORT +size_t crypto_core_hsalsa20_outputbytes(void); + +#define crypto_core_hsalsa20_INPUTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_hsalsa20_inputbytes(void); + +#define crypto_core_hsalsa20_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_core_hsalsa20_keybytes(void); + +#define crypto_core_hsalsa20_CONSTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_hsalsa20_constbytes(void); + +SODIUM_EXPORT +int crypto_core_hsalsa20(unsigned char *out, const unsigned char *in, + const unsigned char *k, const unsigned char *c) + __attribute__ ((nonnull(1, 2, 3))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_keccak1600.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_keccak1600.h new file mode 100644 index 0000000000..46e35ac883 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_keccak1600.h @@ -0,0 +1,66 @@ +#ifndef crypto_core_keccak1600_H +#define crypto_core_keccak1600_H + +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#ifdef __IBMC__ +# pragma pack(1) +#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) +# pragma pack(1) +#else +# pragma pack(push, 1) +#endif + +typedef struct CRYPTO_ALIGN(16) crypto_core_keccak1600_state { + unsigned char opaque[224]; +} crypto_core_keccak1600_state; + +#ifdef __IBMC__ +# pragma pack(pop) +#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) +# pragma pack() +#else +# pragma pack(pop) +#endif + +SODIUM_EXPORT +size_t crypto_core_keccak1600_statebytes(void); + +SODIUM_EXPORT +void crypto_core_keccak1600_init(crypto_core_keccak1600_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_keccak1600_xor_bytes(crypto_core_keccak1600_state *state, + const unsigned char *bytes, + size_t offset, size_t length) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_keccak1600_extract_bytes(const crypto_core_keccak1600_state *state, + unsigned char *bytes, + size_t offset, size_t length) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_keccak1600_permute_24(crypto_core_keccak1600_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_keccak1600_permute_12(crypto_core_keccak1600_state *state) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ristretto255.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ristretto255.h new file mode 100644 index 0000000000..f2820e5576 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ristretto255.h @@ -0,0 +1,100 @@ +#ifndef crypto_core_ristretto255_H +#define crypto_core_ristretto255_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_core_ristretto255_BYTES 32 +SODIUM_EXPORT +size_t crypto_core_ristretto255_bytes(void); + +#define crypto_core_ristretto255_HASHBYTES 64 +SODIUM_EXPORT +size_t crypto_core_ristretto255_hashbytes(void); + +#define crypto_core_ristretto255_SCALARBYTES 32 +SODIUM_EXPORT +size_t crypto_core_ristretto255_scalarbytes(void); + +#define crypto_core_ristretto255_NONREDUCEDSCALARBYTES 64 +SODIUM_EXPORT +size_t crypto_core_ristretto255_nonreducedscalarbytes(void); + +SODIUM_EXPORT +int crypto_core_ristretto255_is_valid_point(const unsigned char *p) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_core_ristretto255_add(unsigned char *r, + const unsigned char *p, const unsigned char *q) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_core_ristretto255_sub(unsigned char *r, + const unsigned char *p, const unsigned char *q) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_core_ristretto255_from_hash(unsigned char *p, + const unsigned char *r) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ristretto255_random(unsigned char *p) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ristretto255_scalar_random(unsigned char *r) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_core_ristretto255_scalar_invert(unsigned char *recip, + const unsigned char *s) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ristretto255_scalar_negate(unsigned char *neg, + const unsigned char *s) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ristretto255_scalar_complement(unsigned char *comp, + const unsigned char *s) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ristretto255_scalar_add(unsigned char *z, + const unsigned char *x, + const unsigned char *y) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ristretto255_scalar_sub(unsigned char *z, + const unsigned char *x, + const unsigned char *y) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_core_ristretto255_scalar_mul(unsigned char *z, + const unsigned char *x, + const unsigned char *y) + __attribute__ ((nonnull)); + +/* + * The interval `s` is sampled from should be at least 317 bits to ensure almost + * uniformity of `r` over `L`. + */ +SODIUM_EXPORT +void crypto_core_ristretto255_scalar_reduce(unsigned char *r, + const unsigned char *s) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa20.h new file mode 100644 index 0000000000..bd79fd9f54 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa20.h @@ -0,0 +1,36 @@ +#ifndef crypto_core_salsa20_H +#define crypto_core_salsa20_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_core_salsa20_OUTPUTBYTES 64U +SODIUM_EXPORT +size_t crypto_core_salsa20_outputbytes(void); + +#define crypto_core_salsa20_INPUTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_salsa20_inputbytes(void); + +#define crypto_core_salsa20_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_core_salsa20_keybytes(void); + +#define crypto_core_salsa20_CONSTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_salsa20_constbytes(void); + +SODIUM_EXPORT +int crypto_core_salsa20(unsigned char *out, const unsigned char *in, + const unsigned char *k, const unsigned char *c) + __attribute__ ((nonnull(1, 2, 3))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa2012.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa2012.h new file mode 100644 index 0000000000..05957591ca --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa2012.h @@ -0,0 +1,36 @@ +#ifndef crypto_core_salsa2012_H +#define crypto_core_salsa2012_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_core_salsa2012_OUTPUTBYTES 64U +SODIUM_EXPORT +size_t crypto_core_salsa2012_outputbytes(void); + +#define crypto_core_salsa2012_INPUTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_salsa2012_inputbytes(void); + +#define crypto_core_salsa2012_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_core_salsa2012_keybytes(void); + +#define crypto_core_salsa2012_CONSTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_salsa2012_constbytes(void); + +SODIUM_EXPORT +int crypto_core_salsa2012(unsigned char *out, const unsigned char *in, + const unsigned char *k, const unsigned char *c) + __attribute__ ((nonnull(1, 2, 3))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa208.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa208.h new file mode 100644 index 0000000000..d2f216af26 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa208.h @@ -0,0 +1,40 @@ +#ifndef crypto_core_salsa208_H +#define crypto_core_salsa208_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_core_salsa208_OUTPUTBYTES 64U +SODIUM_EXPORT +size_t crypto_core_salsa208_outputbytes(void) + __attribute__ ((deprecated)); + +#define crypto_core_salsa208_INPUTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_salsa208_inputbytes(void) + __attribute__ ((deprecated)); + +#define crypto_core_salsa208_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_core_salsa208_keybytes(void) + __attribute__ ((deprecated)); + +#define crypto_core_salsa208_CONSTBYTES 16U +SODIUM_EXPORT +size_t crypto_core_salsa208_constbytes(void) + __attribute__ ((deprecated)); + +SODIUM_EXPORT +int crypto_core_salsa208(unsigned char *out, const unsigned char *in, + const unsigned char *k, const unsigned char *c) + __attribute__ ((nonnull(1, 2, 3))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash.h new file mode 100644 index 0000000000..d897e5d26c --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash.h @@ -0,0 +1,84 @@ +#ifndef crypto_generichash_H +#define crypto_generichash_H + +#include + +#include "crypto_generichash_blake2b.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_generichash_BYTES_MIN crypto_generichash_blake2b_BYTES_MIN +SODIUM_EXPORT +size_t crypto_generichash_bytes_min(void); + +#define crypto_generichash_BYTES_MAX crypto_generichash_blake2b_BYTES_MAX +SODIUM_EXPORT +size_t crypto_generichash_bytes_max(void); + +#define crypto_generichash_BYTES crypto_generichash_blake2b_BYTES +SODIUM_EXPORT +size_t crypto_generichash_bytes(void); + +#define crypto_generichash_KEYBYTES_MIN crypto_generichash_blake2b_KEYBYTES_MIN +SODIUM_EXPORT +size_t crypto_generichash_keybytes_min(void); + +#define crypto_generichash_KEYBYTES_MAX crypto_generichash_blake2b_KEYBYTES_MAX +SODIUM_EXPORT +size_t crypto_generichash_keybytes_max(void); + +#define crypto_generichash_KEYBYTES crypto_generichash_blake2b_KEYBYTES +SODIUM_EXPORT +size_t crypto_generichash_keybytes(void); + +#define crypto_generichash_PRIMITIVE "blake2b" +SODIUM_EXPORT +const char *crypto_generichash_primitive(void); + +/* + * Important when writing bindings for other programming languages: + * the state address should be 64-bytes aligned. + */ +typedef crypto_generichash_blake2b_state crypto_generichash_state; + +SODIUM_EXPORT +size_t crypto_generichash_statebytes(void); + +SODIUM_EXPORT +int crypto_generichash(unsigned char *out, size_t outlen, + const unsigned char *in, unsigned long long inlen, + const unsigned char *key, size_t keylen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_generichash_init(crypto_generichash_state *state, + const unsigned char *key, + const size_t keylen, const size_t outlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_generichash_update(crypto_generichash_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_generichash_final(crypto_generichash_state *state, + unsigned char *out, const size_t outlen) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_generichash_keygen(unsigned char k[crypto_generichash_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash_blake2b.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash_blake2b.h new file mode 100644 index 0000000000..ae3b52f7de --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash_blake2b.h @@ -0,0 +1,122 @@ +#ifndef crypto_generichash_blake2b_H +#define crypto_generichash_blake2b_H + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#ifdef __IBMC__ +# pragma pack(1) +#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) +# pragma pack(1) +#else +# pragma pack(push, 1) +#endif + +typedef struct CRYPTO_ALIGN(64) crypto_generichash_blake2b_state { + unsigned char opaque[384]; +} crypto_generichash_blake2b_state; + +#ifdef __IBMC__ +# pragma pack(pop) +#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) +# pragma pack() +#else +# pragma pack(pop) +#endif + +#define crypto_generichash_blake2b_BYTES_MIN 16U +SODIUM_EXPORT +size_t crypto_generichash_blake2b_bytes_min(void); + +#define crypto_generichash_blake2b_BYTES_MAX 64U +SODIUM_EXPORT +size_t crypto_generichash_blake2b_bytes_max(void); + +#define crypto_generichash_blake2b_BYTES 32U +SODIUM_EXPORT +size_t crypto_generichash_blake2b_bytes(void); + +#define crypto_generichash_blake2b_KEYBYTES_MIN 16U +SODIUM_EXPORT +size_t crypto_generichash_blake2b_keybytes_min(void); + +#define crypto_generichash_blake2b_KEYBYTES_MAX 64U +SODIUM_EXPORT +size_t crypto_generichash_blake2b_keybytes_max(void); + +#define crypto_generichash_blake2b_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_generichash_blake2b_keybytes(void); + +#define crypto_generichash_blake2b_SALTBYTES 16U +SODIUM_EXPORT +size_t crypto_generichash_blake2b_saltbytes(void); + +#define crypto_generichash_blake2b_PERSONALBYTES 16U +SODIUM_EXPORT +size_t crypto_generichash_blake2b_personalbytes(void); + +SODIUM_EXPORT +size_t crypto_generichash_blake2b_statebytes(void); + +SODIUM_EXPORT +int crypto_generichash_blake2b(unsigned char *out, size_t outlen, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *key, size_t keylen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_generichash_blake2b_salt_personal(unsigned char *out, size_t outlen, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *key, + size_t keylen, + const unsigned char *salt, + const unsigned char *personal) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_generichash_blake2b_init(crypto_generichash_blake2b_state *state, + const unsigned char *key, + const size_t keylen, const size_t outlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_generichash_blake2b_init_salt_personal(crypto_generichash_blake2b_state *state, + const unsigned char *key, + const size_t keylen, const size_t outlen, + const unsigned char *salt, + const unsigned char *personal) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_generichash_blake2b_update(crypto_generichash_blake2b_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_generichash_blake2b_final(crypto_generichash_blake2b_state *state, + unsigned char *out, + const size_t outlen) __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_generichash_blake2b_keygen(unsigned char k[crypto_generichash_blake2b_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash.h new file mode 100644 index 0000000000..767d548018 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash.h @@ -0,0 +1,40 @@ +#ifndef crypto_hash_H +#define crypto_hash_H + +/* + * WARNING: Unless you absolutely need to use SHA512 for interoperability, + * purposes, you might want to consider crypto_generichash() instead. + * Unlike SHA512, crypto_generichash() is not vulnerable to length + * extension attacks. + */ + +#include + +#include "crypto_hash_sha512.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_hash_BYTES crypto_hash_sha512_BYTES +SODIUM_EXPORT +size_t crypto_hash_bytes(void); + +SODIUM_EXPORT +int crypto_hash(unsigned char *out, const unsigned char *in, + unsigned long long inlen) __attribute__ ((nonnull(1))); + +#define crypto_hash_PRIMITIVE "sha512" +SODIUM_EXPORT +const char *crypto_hash_primitive(void) + __attribute__ ((warn_unused_result)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha256.h new file mode 100644 index 0000000000..f8311b2e88 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha256.h @@ -0,0 +1,60 @@ +#ifndef crypto_hash_sha256_H +#define crypto_hash_sha256_H + +/* + * WARNING: Unless you absolutely need to use SHA256 for interoperability + * purposes, you might want to consider crypto_generichash() instead. + * Unlike SHA256, crypto_generichash() is not vulnerable to length + * extension attacks. + */ + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct crypto_hash_sha256_state { + uint32_t state[8]; + uint64_t count; + uint8_t buf[64]; +} crypto_hash_sha256_state; + +SODIUM_EXPORT +size_t crypto_hash_sha256_statebytes(void); + +#define crypto_hash_sha256_BYTES 32U +SODIUM_EXPORT +size_t crypto_hash_sha256_bytes(void); + +SODIUM_EXPORT +int crypto_hash_sha256(unsigned char *out, const unsigned char *in, + unsigned long long inlen) __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_hash_sha256_init(crypto_hash_sha256_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_hash_sha256_update(crypto_hash_sha256_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_hash_sha256_final(crypto_hash_sha256_state *state, + unsigned char *out) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha512.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha512.h new file mode 100644 index 0000000000..1628901cf2 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha512.h @@ -0,0 +1,60 @@ +#ifndef crypto_hash_sha512_H +#define crypto_hash_sha512_H + +/* + * WARNING: Unless you absolutely need to use SHA512 for interoperability + * purposes, you might want to consider crypto_generichash() instead. + * Unlike SHA512, crypto_generichash() is not vulnerable to length + * extension attacks. + */ + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct crypto_hash_sha512_state { + uint64_t state[8]; + uint64_t count[2]; + uint8_t buf[128]; +} crypto_hash_sha512_state; + +SODIUM_EXPORT +size_t crypto_hash_sha512_statebytes(void); + +#define crypto_hash_sha512_BYTES 64U +SODIUM_EXPORT +size_t crypto_hash_sha512_bytes(void); + +SODIUM_EXPORT +int crypto_hash_sha512(unsigned char *out, const unsigned char *in, + unsigned long long inlen) __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_hash_sha512_init(crypto_hash_sha512_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_hash_sha512_update(crypto_hash_sha512_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_hash_sha512_final(crypto_hash_sha512_state *state, + unsigned char *out) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_ipcrypt.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_ipcrypt.h new file mode 100644 index 0000000000..65f526d2a6 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_ipcrypt.h @@ -0,0 +1,132 @@ +#ifndef crypto_ipcrypt_H +#define crypto_ipcrypt_H + +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_ipcrypt_BYTES 16U +SODIUM_EXPORT +size_t crypto_ipcrypt_bytes(void); + +#define crypto_ipcrypt_KEYBYTES 16U +SODIUM_EXPORT +size_t crypto_ipcrypt_keybytes(void); + +#define crypto_ipcrypt_ND_KEYBYTES 16U +SODIUM_EXPORT +size_t crypto_ipcrypt_nd_keybytes(void); + +#define crypto_ipcrypt_ND_TWEAKBYTES 8U +SODIUM_EXPORT +size_t crypto_ipcrypt_nd_tweakbytes(void); + +#define crypto_ipcrypt_ND_INPUTBYTES 16U +SODIUM_EXPORT +size_t crypto_ipcrypt_nd_inputbytes(void); + +#define crypto_ipcrypt_ND_OUTPUTBYTES 24U +SODIUM_EXPORT +size_t crypto_ipcrypt_nd_outputbytes(void); + +#define crypto_ipcrypt_NDX_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_ipcrypt_ndx_keybytes(void); + +#define crypto_ipcrypt_NDX_TWEAKBYTES 16U +SODIUM_EXPORT +size_t crypto_ipcrypt_ndx_tweakbytes(void); + +#define crypto_ipcrypt_NDX_INPUTBYTES 16U +SODIUM_EXPORT +size_t crypto_ipcrypt_ndx_inputbytes(void); + +#define crypto_ipcrypt_NDX_OUTPUTBYTES 32U +SODIUM_EXPORT +size_t crypto_ipcrypt_ndx_outputbytes(void); + +#define crypto_ipcrypt_PFX_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_ipcrypt_pfx_keybytes(void); + +#define crypto_ipcrypt_PFX_BYTES 16U +SODIUM_EXPORT +size_t crypto_ipcrypt_pfx_bytes(void); + +SODIUM_EXPORT +void crypto_ipcrypt_keygen(unsigned char k[crypto_ipcrypt_KEYBYTES]) __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_nd_keygen(unsigned char k[crypto_ipcrypt_ND_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_ndx_keygen(unsigned char k[crypto_ipcrypt_NDX_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_pfx_keygen(unsigned char k[crypto_ipcrypt_PFX_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_encrypt(unsigned char out[crypto_ipcrypt_BYTES], + const unsigned char in[crypto_ipcrypt_BYTES], + const unsigned char k[crypto_ipcrypt_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_decrypt(unsigned char out[crypto_ipcrypt_BYTES], + const unsigned char in[crypto_ipcrypt_BYTES], + const unsigned char k[crypto_ipcrypt_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_nd_encrypt(unsigned char out[crypto_ipcrypt_ND_OUTPUTBYTES], + const unsigned char in[crypto_ipcrypt_ND_INPUTBYTES], + const unsigned char t[crypto_ipcrypt_ND_TWEAKBYTES], + const unsigned char k[crypto_ipcrypt_ND_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_nd_decrypt(unsigned char out[crypto_ipcrypt_ND_INPUTBYTES], + const unsigned char in[crypto_ipcrypt_ND_OUTPUTBYTES], + const unsigned char k[crypto_ipcrypt_ND_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_ndx_encrypt(unsigned char out[crypto_ipcrypt_NDX_OUTPUTBYTES], + const unsigned char in[crypto_ipcrypt_NDX_INPUTBYTES], + const unsigned char t[crypto_ipcrypt_NDX_TWEAKBYTES], + const unsigned char k[crypto_ipcrypt_NDX_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_ndx_decrypt(unsigned char out[crypto_ipcrypt_NDX_INPUTBYTES], + const unsigned char in[crypto_ipcrypt_NDX_OUTPUTBYTES], + const unsigned char k[crypto_ipcrypt_NDX_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_pfx_encrypt(unsigned char out[crypto_ipcrypt_PFX_BYTES], + const unsigned char in[crypto_ipcrypt_PFX_BYTES], + const unsigned char k[crypto_ipcrypt_PFX_KEYBYTES]) + __attribute__((nonnull)); + +SODIUM_EXPORT +void crypto_ipcrypt_pfx_decrypt(unsigned char out[crypto_ipcrypt_PFX_BYTES], + const unsigned char in[crypto_ipcrypt_PFX_BYTES], + const unsigned char k[crypto_ipcrypt_PFX_KEYBYTES]) + __attribute__((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf.h new file mode 100644 index 0000000000..ac2fc6183c --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf.h @@ -0,0 +1,53 @@ +#ifndef crypto_kdf_H +#define crypto_kdf_H + +#include +#include + +#include "crypto_kdf_blake2b.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_kdf_BYTES_MIN crypto_kdf_blake2b_BYTES_MIN +SODIUM_EXPORT +size_t crypto_kdf_bytes_min(void); + +#define crypto_kdf_BYTES_MAX crypto_kdf_blake2b_BYTES_MAX +SODIUM_EXPORT +size_t crypto_kdf_bytes_max(void); + +#define crypto_kdf_CONTEXTBYTES crypto_kdf_blake2b_CONTEXTBYTES +SODIUM_EXPORT +size_t crypto_kdf_contextbytes(void); + +#define crypto_kdf_KEYBYTES crypto_kdf_blake2b_KEYBYTES +SODIUM_EXPORT +size_t crypto_kdf_keybytes(void); + +#define crypto_kdf_PRIMITIVE "blake2b" +SODIUM_EXPORT +const char *crypto_kdf_primitive(void) + __attribute__ ((warn_unused_result)); + +SODIUM_EXPORT +int crypto_kdf_derive_from_key(unsigned char *subkey, size_t subkey_len, + uint64_t subkey_id, + const char ctx[crypto_kdf_CONTEXTBYTES], + const unsigned char key[crypto_kdf_KEYBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_kdf_keygen(unsigned char k[crypto_kdf_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_blake2b.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_blake2b.h new file mode 100644 index 0000000000..489c7c20fc --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_blake2b.h @@ -0,0 +1,44 @@ +#ifndef crypto_kdf_blake2b_H +#define crypto_kdf_blake2b_H + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_kdf_blake2b_BYTES_MIN 16 +SODIUM_EXPORT +size_t crypto_kdf_blake2b_bytes_min(void); + +#define crypto_kdf_blake2b_BYTES_MAX 64 +SODIUM_EXPORT +size_t crypto_kdf_blake2b_bytes_max(void); + +#define crypto_kdf_blake2b_CONTEXTBYTES 8 +SODIUM_EXPORT +size_t crypto_kdf_blake2b_contextbytes(void); + +#define crypto_kdf_blake2b_KEYBYTES 32 +SODIUM_EXPORT +size_t crypto_kdf_blake2b_keybytes(void); + +SODIUM_EXPORT +int crypto_kdf_blake2b_derive_from_key(unsigned char *subkey, size_t subkey_len, + uint64_t subkey_id, + const char ctx[crypto_kdf_blake2b_CONTEXTBYTES], + const unsigned char key[crypto_kdf_blake2b_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha256.h new file mode 100644 index 0000000000..e7e7f4db6d --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha256.h @@ -0,0 +1,74 @@ +#ifndef crypto_kdf_hkdf_sha256_H +#define crypto_kdf_hkdf_sha256_H + +#include +#include +#include + +#include "crypto_kdf.h" +#include "crypto_auth_hmacsha256.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_kdf_hkdf_sha256_KEYBYTES crypto_auth_hmacsha256_BYTES +SODIUM_EXPORT +size_t crypto_kdf_hkdf_sha256_keybytes(void); + +#define crypto_kdf_hkdf_sha256_BYTES_MIN 0U +SODIUM_EXPORT +size_t crypto_kdf_hkdf_sha256_bytes_min(void); + +#define crypto_kdf_hkdf_sha256_BYTES_MAX (0xff * crypto_auth_hmacsha256_BYTES) +SODIUM_EXPORT +size_t crypto_kdf_hkdf_sha256_bytes_max(void); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha256_extract(unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES], + const unsigned char *salt, size_t salt_len, + const unsigned char *ikm, size_t ikm_len) + __attribute__ ((nonnull(4))); + +SODIUM_EXPORT +void crypto_kdf_hkdf_sha256_keygen(unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES]); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha256_expand(unsigned char *out, size_t out_len, + const char *ctx, size_t ctx_len, + const unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES]) + __attribute__ ((nonnull(1))); + +/* ------------------------------------------------------------------------- */ + +typedef struct crypto_kdf_hkdf_sha256_state { + crypto_auth_hmacsha256_state st; +} crypto_kdf_hkdf_sha256_state; + +SODIUM_EXPORT +size_t crypto_kdf_hkdf_sha256_statebytes(void); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha256_extract_init(crypto_kdf_hkdf_sha256_state *state, + const unsigned char *salt, size_t salt_len) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha256_extract_update(crypto_kdf_hkdf_sha256_state *state, + const unsigned char *ikm, size_t ikm_len) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha256_extract_final(crypto_kdf_hkdf_sha256_state *state, + unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha512.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha512.h new file mode 100644 index 0000000000..0ed205dfb8 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha512.h @@ -0,0 +1,75 @@ +#ifndef crypto_kdf_hkdf_sha512_H +#define crypto_kdf_hkdf_sha512_H + +#include +#include +#include + +#include "crypto_kdf.h" +#include "crypto_auth_hmacsha512.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_kdf_hkdf_sha512_KEYBYTES crypto_auth_hmacsha512_BYTES +SODIUM_EXPORT +size_t crypto_kdf_hkdf_sha512_keybytes(void); + +#define crypto_kdf_hkdf_sha512_BYTES_MIN 0U +SODIUM_EXPORT +size_t crypto_kdf_hkdf_sha512_bytes_min(void); + +#define crypto_kdf_hkdf_sha512_BYTES_MAX (0xff * crypto_auth_hmacsha512_BYTES) +SODIUM_EXPORT +size_t crypto_kdf_hkdf_sha512_bytes_max(void); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha512_extract(unsigned char prk[crypto_kdf_hkdf_sha512_KEYBYTES], + const unsigned char *salt, size_t salt_len, + const unsigned char *ikm, size_t ikm_len) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +void crypto_kdf_hkdf_sha512_keygen(unsigned char prk[crypto_kdf_hkdf_sha512_KEYBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha512_expand(unsigned char *out, size_t out_len, + const char *ctx, size_t ctx_len, + const unsigned char prk[crypto_kdf_hkdf_sha512_KEYBYTES]) + __attribute__ ((nonnull(1))); + +/* ------------------------------------------------------------------------- */ + +typedef struct crypto_kdf_hkdf_sha512_state { + crypto_auth_hmacsha512_state st; +} crypto_kdf_hkdf_sha512_state; + +SODIUM_EXPORT +size_t crypto_kdf_hkdf_sha512_statebytes(void); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha512_extract_init(crypto_kdf_hkdf_sha512_state *state, + const unsigned char *salt, size_t salt_len) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha512_extract_update(crypto_kdf_hkdf_sha512_state *state, + const unsigned char *ikm, size_t ikm_len) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_kdf_hkdf_sha512_extract_final(crypto_kdf_hkdf_sha512_state *state, + unsigned char prk[crypto_kdf_hkdf_sha512_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kx.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kx.h new file mode 100644 index 0000000000..347132c320 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kx.h @@ -0,0 +1,66 @@ +#ifndef crypto_kx_H +#define crypto_kx_H + +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_kx_PUBLICKEYBYTES 32 +SODIUM_EXPORT +size_t crypto_kx_publickeybytes(void); + +#define crypto_kx_SECRETKEYBYTES 32 +SODIUM_EXPORT +size_t crypto_kx_secretkeybytes(void); + +#define crypto_kx_SEEDBYTES 32 +SODIUM_EXPORT +size_t crypto_kx_seedbytes(void); + +#define crypto_kx_SESSIONKEYBYTES 32 +SODIUM_EXPORT +size_t crypto_kx_sessionkeybytes(void); + +#define crypto_kx_PRIMITIVE "x25519blake2b" +SODIUM_EXPORT +const char *crypto_kx_primitive(void); + +SODIUM_EXPORT +int crypto_kx_seed_keypair(unsigned char pk[crypto_kx_PUBLICKEYBYTES], + unsigned char sk[crypto_kx_SECRETKEYBYTES], + const unsigned char seed[crypto_kx_SEEDBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_kx_keypair(unsigned char pk[crypto_kx_PUBLICKEYBYTES], + unsigned char sk[crypto_kx_SECRETKEYBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_kx_client_session_keys(unsigned char rx[crypto_kx_SESSIONKEYBYTES], + unsigned char tx[crypto_kx_SESSIONKEYBYTES], + const unsigned char client_pk[crypto_kx_PUBLICKEYBYTES], + const unsigned char client_sk[crypto_kx_SECRETKEYBYTES], + const unsigned char server_pk[crypto_kx_PUBLICKEYBYTES]) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 4, 5))); + +SODIUM_EXPORT +int crypto_kx_server_session_keys(unsigned char rx[crypto_kx_SESSIONKEYBYTES], + unsigned char tx[crypto_kx_SESSIONKEYBYTES], + const unsigned char server_pk[crypto_kx_PUBLICKEYBYTES], + const unsigned char server_sk[crypto_kx_SECRETKEYBYTES], + const unsigned char client_pk[crypto_kx_PUBLICKEYBYTES]) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 4, 5))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth.h new file mode 100644 index 0000000000..7cd7b07060 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth.h @@ -0,0 +1,65 @@ +#ifndef crypto_onetimeauth_H +#define crypto_onetimeauth_H + +#include + +#include "crypto_onetimeauth_poly1305.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef crypto_onetimeauth_poly1305_state crypto_onetimeauth_state; + +SODIUM_EXPORT +size_t crypto_onetimeauth_statebytes(void); + +#define crypto_onetimeauth_BYTES crypto_onetimeauth_poly1305_BYTES +SODIUM_EXPORT +size_t crypto_onetimeauth_bytes(void); + +#define crypto_onetimeauth_KEYBYTES crypto_onetimeauth_poly1305_KEYBYTES +SODIUM_EXPORT +size_t crypto_onetimeauth_keybytes(void); + +#define crypto_onetimeauth_PRIMITIVE "poly1305" +SODIUM_EXPORT +const char *crypto_onetimeauth_primitive(void); + +SODIUM_EXPORT +int crypto_onetimeauth(unsigned char *out, const unsigned char *in, + unsigned long long inlen, const unsigned char *k) + __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_onetimeauth_verify(const unsigned char *h, const unsigned char *in, + unsigned long long inlen, const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_onetimeauth_init(crypto_onetimeauth_state *state, + const unsigned char *key) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_onetimeauth_update(crypto_onetimeauth_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_onetimeauth_final(crypto_onetimeauth_state *state, + unsigned char *out) __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_onetimeauth_keygen(unsigned char k[crypto_onetimeauth_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth_poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth_poly1305.h new file mode 100644 index 0000000000..1565d37aba --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth_poly1305.h @@ -0,0 +1,72 @@ +#ifndef crypto_onetimeauth_poly1305_H +#define crypto_onetimeauth_poly1305_H + +#include +#include +#include + +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct CRYPTO_ALIGN(16) crypto_onetimeauth_poly1305_state { + unsigned char opaque[256]; +} crypto_onetimeauth_poly1305_state; + +SODIUM_EXPORT +size_t crypto_onetimeauth_poly1305_statebytes(void); + +#define crypto_onetimeauth_poly1305_BYTES 16U +SODIUM_EXPORT +size_t crypto_onetimeauth_poly1305_bytes(void); + +#define crypto_onetimeauth_poly1305_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_onetimeauth_poly1305_keybytes(void); + +SODIUM_EXPORT +int crypto_onetimeauth_poly1305(unsigned char *out, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *k) + __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_onetimeauth_poly1305_verify(const unsigned char *h, + const unsigned char *in, + unsigned long long inlen, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_onetimeauth_poly1305_init(crypto_onetimeauth_poly1305_state *state, + const unsigned char *key) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_onetimeauth_poly1305_update(crypto_onetimeauth_poly1305_state *state, + const unsigned char *in, + unsigned long long inlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_onetimeauth_poly1305_final(crypto_onetimeauth_poly1305_state *state, + unsigned char *out) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void crypto_onetimeauth_poly1305_keygen(unsigned char k[crypto_onetimeauth_poly1305_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash.h new file mode 100644 index 0000000000..9683192bf5 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash.h @@ -0,0 +1,147 @@ +#ifndef crypto_pwhash_H +#define crypto_pwhash_H + +#include + +#include "crypto_pwhash_argon2i.h" +#include "crypto_pwhash_argon2id.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_pwhash_ALG_ARGON2I13 crypto_pwhash_argon2i_ALG_ARGON2I13 +SODIUM_EXPORT +int crypto_pwhash_alg_argon2i13(void); + +#define crypto_pwhash_ALG_ARGON2ID13 crypto_pwhash_argon2id_ALG_ARGON2ID13 +SODIUM_EXPORT +int crypto_pwhash_alg_argon2id13(void); + +#define crypto_pwhash_ALG_DEFAULT crypto_pwhash_ALG_ARGON2ID13 +SODIUM_EXPORT +int crypto_pwhash_alg_default(void); + +#define crypto_pwhash_BYTES_MIN crypto_pwhash_argon2id_BYTES_MIN +SODIUM_EXPORT +size_t crypto_pwhash_bytes_min(void); + +#define crypto_pwhash_BYTES_MAX crypto_pwhash_argon2id_BYTES_MAX +SODIUM_EXPORT +size_t crypto_pwhash_bytes_max(void); + +#define crypto_pwhash_PASSWD_MIN crypto_pwhash_argon2id_PASSWD_MIN +SODIUM_EXPORT +size_t crypto_pwhash_passwd_min(void); + +#define crypto_pwhash_PASSWD_MAX crypto_pwhash_argon2id_PASSWD_MAX +SODIUM_EXPORT +size_t crypto_pwhash_passwd_max(void); + +#define crypto_pwhash_SALTBYTES crypto_pwhash_argon2id_SALTBYTES +SODIUM_EXPORT +size_t crypto_pwhash_saltbytes(void); + +#define crypto_pwhash_STRBYTES crypto_pwhash_argon2id_STRBYTES +SODIUM_EXPORT +size_t crypto_pwhash_strbytes(void); + +#define crypto_pwhash_STRPREFIX crypto_pwhash_argon2id_STRPREFIX +SODIUM_EXPORT +const char *crypto_pwhash_strprefix(void); + +#define crypto_pwhash_OPSLIMIT_MIN crypto_pwhash_argon2id_OPSLIMIT_MIN +SODIUM_EXPORT +size_t crypto_pwhash_opslimit_min(void); + +#define crypto_pwhash_OPSLIMIT_MAX crypto_pwhash_argon2id_OPSLIMIT_MAX +SODIUM_EXPORT +size_t crypto_pwhash_opslimit_max(void); + +#define crypto_pwhash_MEMLIMIT_MIN crypto_pwhash_argon2id_MEMLIMIT_MIN +SODIUM_EXPORT +size_t crypto_pwhash_memlimit_min(void); + +#define crypto_pwhash_MEMLIMIT_MAX crypto_pwhash_argon2id_MEMLIMIT_MAX +SODIUM_EXPORT +size_t crypto_pwhash_memlimit_max(void); + +#define crypto_pwhash_OPSLIMIT_INTERACTIVE crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE +SODIUM_EXPORT +size_t crypto_pwhash_opslimit_interactive(void); + +#define crypto_pwhash_MEMLIMIT_INTERACTIVE crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE +SODIUM_EXPORT +size_t crypto_pwhash_memlimit_interactive(void); + +#define crypto_pwhash_OPSLIMIT_MODERATE crypto_pwhash_argon2id_OPSLIMIT_MODERATE +SODIUM_EXPORT +size_t crypto_pwhash_opslimit_moderate(void); + +#define crypto_pwhash_MEMLIMIT_MODERATE crypto_pwhash_argon2id_MEMLIMIT_MODERATE +SODIUM_EXPORT +size_t crypto_pwhash_memlimit_moderate(void); + +#define crypto_pwhash_OPSLIMIT_SENSITIVE crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE +SODIUM_EXPORT +size_t crypto_pwhash_opslimit_sensitive(void); + +#define crypto_pwhash_MEMLIMIT_SENSITIVE crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE +SODIUM_EXPORT +size_t crypto_pwhash_memlimit_sensitive(void); + +/* + * With this function, do not forget to store all parameters, including the + * algorithm identifier in order to produce deterministic output. + * The crypto_pwhash_* definitions, including crypto_pwhash_ALG_DEFAULT, + * may change. + */ +SODIUM_EXPORT +int crypto_pwhash(unsigned char * const out, unsigned long long outlen, + const char * const passwd, unsigned long long passwdlen, + const unsigned char * const salt, + unsigned long long opslimit, size_t memlimit, int alg) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +/* + * The output string already includes all the required parameters, including + * the algorithm identifier. The string is all that has to be stored in + * order to verify a password. + */ +SODIUM_EXPORT +int crypto_pwhash_str(char out[crypto_pwhash_STRBYTES], + const char * const passwd, unsigned long long passwdlen, + unsigned long long opslimit, size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_str_alg(char out[crypto_pwhash_STRBYTES], + const char * const passwd, unsigned long long passwdlen, + unsigned long long opslimit, size_t memlimit, int alg) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_str_verify(const char *str, + const char * const passwd, + unsigned long long passwdlen) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_str_needs_rehash(const char *str, + unsigned long long opslimit, size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#define crypto_pwhash_PRIMITIVE "argon2id,argon2i" +SODIUM_EXPORT +const char *crypto_pwhash_primitive(void) + __attribute__ ((warn_unused_result)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2i.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2i.h new file mode 100644 index 0000000000..63abdf4ab9 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2i.h @@ -0,0 +1,122 @@ +#ifndef crypto_pwhash_argon2i_H +#define crypto_pwhash_argon2i_H + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_pwhash_argon2i_ALG_ARGON2I13 1 +SODIUM_EXPORT +int crypto_pwhash_argon2i_alg_argon2i13(void); + +#define crypto_pwhash_argon2i_BYTES_MIN 16U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_bytes_min(void); + +#define crypto_pwhash_argon2i_BYTES_MAX SODIUM_MIN(SODIUM_SIZE_MAX, 4294967295U) +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_bytes_max(void); + +#define crypto_pwhash_argon2i_PASSWD_MIN 0U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_passwd_min(void); + +#define crypto_pwhash_argon2i_PASSWD_MAX 4294967295U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_passwd_max(void); + +#define crypto_pwhash_argon2i_SALTBYTES 16U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_saltbytes(void); + +#define crypto_pwhash_argon2i_STRBYTES 128U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_strbytes(void); + +#define crypto_pwhash_argon2i_STRPREFIX "$argon2i$" +SODIUM_EXPORT +const char *crypto_pwhash_argon2i_strprefix(void); + +#define crypto_pwhash_argon2i_OPSLIMIT_MIN 3U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_opslimit_min(void); + +#define crypto_pwhash_argon2i_OPSLIMIT_MAX 4294967295U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_opslimit_max(void); + +#define crypto_pwhash_argon2i_MEMLIMIT_MIN 8192U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_memlimit_min(void); + +#define crypto_pwhash_argon2i_MEMLIMIT_MAX \ + ((SIZE_MAX >= 4398046510080U) ? 4398046510080U : (SIZE_MAX >= 2147483648U) ? 2147483648U : 32768U) +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_memlimit_max(void); + +#define crypto_pwhash_argon2i_OPSLIMIT_INTERACTIVE 4U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_opslimit_interactive(void); + +#define crypto_pwhash_argon2i_MEMLIMIT_INTERACTIVE 33554432U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_memlimit_interactive(void); + +#define crypto_pwhash_argon2i_OPSLIMIT_MODERATE 6U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_opslimit_moderate(void); + +#define crypto_pwhash_argon2i_MEMLIMIT_MODERATE 134217728U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_memlimit_moderate(void); + +#define crypto_pwhash_argon2i_OPSLIMIT_SENSITIVE 8U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_opslimit_sensitive(void); + +#define crypto_pwhash_argon2i_MEMLIMIT_SENSITIVE 536870912U +SODIUM_EXPORT +size_t crypto_pwhash_argon2i_memlimit_sensitive(void); + +SODIUM_EXPORT +int crypto_pwhash_argon2i(unsigned char * const out, + unsigned long long outlen, + const char * const passwd, + unsigned long long passwdlen, + const unsigned char * const salt, + unsigned long long opslimit, size_t memlimit, + int alg) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_argon2i_str(char out[crypto_pwhash_argon2i_STRBYTES], + const char * const passwd, + unsigned long long passwdlen, + unsigned long long opslimit, size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_argon2i_str_verify(const char * str, + const char * const passwd, + unsigned long long passwdlen) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_argon2i_str_needs_rehash(const char * str, + unsigned long long opslimit, size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2id.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2id.h new file mode 100644 index 0000000000..98bc83e075 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2id.h @@ -0,0 +1,122 @@ +#ifndef crypto_pwhash_argon2id_H +#define crypto_pwhash_argon2id_H + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_pwhash_argon2id_ALG_ARGON2ID13 2 +SODIUM_EXPORT +int crypto_pwhash_argon2id_alg_argon2id13(void); + +#define crypto_pwhash_argon2id_BYTES_MIN 16U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_bytes_min(void); + +#define crypto_pwhash_argon2id_BYTES_MAX SODIUM_MIN(SODIUM_SIZE_MAX, 4294967295U) +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_bytes_max(void); + +#define crypto_pwhash_argon2id_PASSWD_MIN 0U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_passwd_min(void); + +#define crypto_pwhash_argon2id_PASSWD_MAX 4294967295U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_passwd_max(void); + +#define crypto_pwhash_argon2id_SALTBYTES 16U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_saltbytes(void); + +#define crypto_pwhash_argon2id_STRBYTES 128U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_strbytes(void); + +#define crypto_pwhash_argon2id_STRPREFIX "$argon2id$" +SODIUM_EXPORT +const char *crypto_pwhash_argon2id_strprefix(void); + +#define crypto_pwhash_argon2id_OPSLIMIT_MIN 1U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_opslimit_min(void); + +#define crypto_pwhash_argon2id_OPSLIMIT_MAX 4294967295U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_opslimit_max(void); + +#define crypto_pwhash_argon2id_MEMLIMIT_MIN 8192U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_memlimit_min(void); + +#define crypto_pwhash_argon2id_MEMLIMIT_MAX \ + ((SIZE_MAX >= 4398046510080U) ? 4398046510080U : (SIZE_MAX >= 2147483648U) ? 2147483648U : 32768U) +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_memlimit_max(void); + +#define crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE 2U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_opslimit_interactive(void); + +#define crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE 67108864U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_memlimit_interactive(void); + +#define crypto_pwhash_argon2id_OPSLIMIT_MODERATE 3U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_opslimit_moderate(void); + +#define crypto_pwhash_argon2id_MEMLIMIT_MODERATE 268435456U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_memlimit_moderate(void); + +#define crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE 4U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_opslimit_sensitive(void); + +#define crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE 1073741824U +SODIUM_EXPORT +size_t crypto_pwhash_argon2id_memlimit_sensitive(void); + +SODIUM_EXPORT +int crypto_pwhash_argon2id(unsigned char * const out, + unsigned long long outlen, + const char * const passwd, + unsigned long long passwdlen, + const unsigned char * const salt, + unsigned long long opslimit, size_t memlimit, + int alg) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_argon2id_str(char out[crypto_pwhash_argon2id_STRBYTES], + const char * const passwd, + unsigned long long passwdlen, + unsigned long long opslimit, size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_argon2id_str_verify(const char * str, + const char * const passwd, + unsigned long long passwdlen) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_argon2id_str_needs_rehash(const char * str, + unsigned long long opslimit, size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_scryptsalsa208sha256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_scryptsalsa208sha256.h new file mode 100644 index 0000000000..8956dfbf74 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_scryptsalsa208sha256.h @@ -0,0 +1,120 @@ +#ifndef crypto_pwhash_scryptsalsa208sha256_H +#define crypto_pwhash_scryptsalsa208sha256_H + +#include +#include +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_pwhash_scryptsalsa208sha256_BYTES_MIN 16U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_bytes_min(void); + +#define crypto_pwhash_scryptsalsa208sha256_BYTES_MAX \ + SODIUM_MIN(SODIUM_SIZE_MAX, 0x1fffffffe0ULL) +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_bytes_max(void); + +#define crypto_pwhash_scryptsalsa208sha256_PASSWD_MIN 0U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_passwd_min(void); + +#define crypto_pwhash_scryptsalsa208sha256_PASSWD_MAX SODIUM_SIZE_MAX +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_passwd_max(void); + +#define crypto_pwhash_scryptsalsa208sha256_SALTBYTES 32U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_saltbytes(void); + +#define crypto_pwhash_scryptsalsa208sha256_STRBYTES 102U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_strbytes(void); + +#define crypto_pwhash_scryptsalsa208sha256_STRPREFIX "$7$" +SODIUM_EXPORT +const char *crypto_pwhash_scryptsalsa208sha256_strprefix(void); + +#define crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_MIN 32768U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_opslimit_min(void); + +#define crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_MAX 4294967295U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_opslimit_max(void); + +#define crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_MIN 16777216U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_memlimit_min(void); + +#define crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_MAX \ + SODIUM_MIN(SIZE_MAX, 68719476736ULL) +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_memlimit_max(void); + +#define crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE 524288U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_opslimit_interactive(void); + +#define crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE 16777216U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_memlimit_interactive(void); + +#define crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_SENSITIVE 33554432U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive(void); + +#define crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_SENSITIVE 1073741824U +SODIUM_EXPORT +size_t crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive(void); + +SODIUM_EXPORT +int crypto_pwhash_scryptsalsa208sha256(unsigned char * const out, + unsigned long long outlen, + const char * const passwd, + unsigned long long passwdlen, + const unsigned char * const salt, + unsigned long long opslimit, + size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_scryptsalsa208sha256_str(char out[crypto_pwhash_scryptsalsa208sha256_STRBYTES], + const char * const passwd, + unsigned long long passwdlen, + unsigned long long opslimit, + size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_scryptsalsa208sha256_str_verify(const char * str, + const char * const passwd, + unsigned long long passwdlen) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_scryptsalsa208sha256_ll(const uint8_t * passwd, size_t passwdlen, + const uint8_t * salt, size_t saltlen, + uint64_t N, uint32_t r, uint32_t p, + uint8_t * buf, size_t buflen) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_pwhash_scryptsalsa208sha256_str_needs_rehash(const char * str, + unsigned long long opslimit, + size_t memlimit) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult.h new file mode 100644 index 0000000000..1c68585378 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult.h @@ -0,0 +1,46 @@ +#ifndef crypto_scalarmult_H +#define crypto_scalarmult_H + +#include + +#include "crypto_scalarmult_curve25519.h" +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_scalarmult_BYTES crypto_scalarmult_curve25519_BYTES +SODIUM_EXPORT +size_t crypto_scalarmult_bytes(void); + +#define crypto_scalarmult_SCALARBYTES crypto_scalarmult_curve25519_SCALARBYTES +SODIUM_EXPORT +size_t crypto_scalarmult_scalarbytes(void); + +#define crypto_scalarmult_PRIMITIVE "curve25519" +SODIUM_EXPORT +const char *crypto_scalarmult_primitive(void); + +SODIUM_EXPORT +int crypto_scalarmult_base(unsigned char *q, const unsigned char *n) + __attribute__ ((nonnull)); + +/* + * NOTE: Do not use the result of this function directly for key exchange. + * + * Hash the result with the public keys in order to compute a shared + * secret key: H(q || client_pk || server_pk) + * + * Or unless this is not an option, use the crypto_kx() API instead. + */ +SODIUM_EXPORT +int crypto_scalarmult(unsigned char *q, const unsigned char *n, + const unsigned char *p) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_curve25519.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_curve25519.h new file mode 100644 index 0000000000..60e9d0c5a4 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_curve25519.h @@ -0,0 +1,42 @@ +#ifndef crypto_scalarmult_curve25519_H +#define crypto_scalarmult_curve25519_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_scalarmult_curve25519_BYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_curve25519_bytes(void); + +#define crypto_scalarmult_curve25519_SCALARBYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_curve25519_scalarbytes(void); + +/* + * NOTE: Do not use the result of this function directly for key exchange. + * + * Hash the result with the public keys in order to compute a shared + * secret key: H(q || client_pk || server_pk) + * + * Or unless this is not an option, use the crypto_kx() API instead. + */ +SODIUM_EXPORT +int crypto_scalarmult_curve25519(unsigned char *q, const unsigned char *n, + const unsigned char *p) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_scalarmult_curve25519_base(unsigned char *q, + const unsigned char *n) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ed25519.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ed25519.h new file mode 100644 index 0000000000..2dfa4d7073 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ed25519.h @@ -0,0 +1,51 @@ + +#ifndef crypto_scalarmult_ed25519_H +#define crypto_scalarmult_ed25519_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_scalarmult_ed25519_BYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_ed25519_bytes(void); + +#define crypto_scalarmult_ed25519_SCALARBYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_ed25519_scalarbytes(void); + +/* + * NOTE: Do not use the result of this function directly for key exchange. + * + * Hash the result with the public keys in order to compute a shared + * secret key: H(q || client_pk || server_pk) + * + * Or unless this is not an option, use the crypto_kx() API instead. + */ +SODIUM_EXPORT +int crypto_scalarmult_ed25519(unsigned char *q, const unsigned char *n, + const unsigned char *p) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_scalarmult_ed25519_noclamp(unsigned char *q, const unsigned char *n, + const unsigned char *p) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_scalarmult_ed25519_base(unsigned char *q, const unsigned char *n) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_scalarmult_ed25519_base_noclamp(unsigned char *q, const unsigned char *n) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ristretto255.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ristretto255.h new file mode 100644 index 0000000000..40a45ccef0 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ristretto255.h @@ -0,0 +1,43 @@ + +#ifndef crypto_scalarmult_ristretto255_H +#define crypto_scalarmult_ristretto255_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_scalarmult_ristretto255_BYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_ristretto255_bytes(void); + +#define crypto_scalarmult_ristretto255_SCALARBYTES 32U +SODIUM_EXPORT +size_t crypto_scalarmult_ristretto255_scalarbytes(void); + +/* + * NOTE: Do not use the result of this function directly for key exchange. + * + * Hash the result with the public keys in order to compute a shared + * secret key: H(q || client_pk || server_pk) + * + * Or unless this is not an option, use the crypto_kx() API instead. + */ +SODIUM_EXPORT +int crypto_scalarmult_ristretto255(unsigned char *q, const unsigned char *n, + const unsigned char *p) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_scalarmult_ristretto255_base(unsigned char *q, + const unsigned char *n) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox.h new file mode 100644 index 0000000000..1d3709db12 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox.h @@ -0,0 +1,93 @@ +#ifndef crypto_secretbox_H +#define crypto_secretbox_H + +#include + +#include "crypto_secretbox_xsalsa20poly1305.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_secretbox_KEYBYTES crypto_secretbox_xsalsa20poly1305_KEYBYTES +SODIUM_EXPORT +size_t crypto_secretbox_keybytes(void); + +#define crypto_secretbox_NONCEBYTES crypto_secretbox_xsalsa20poly1305_NONCEBYTES +SODIUM_EXPORT +size_t crypto_secretbox_noncebytes(void); + +#define crypto_secretbox_MACBYTES crypto_secretbox_xsalsa20poly1305_MACBYTES +SODIUM_EXPORT +size_t crypto_secretbox_macbytes(void); + +#define crypto_secretbox_PRIMITIVE "xsalsa20poly1305" +SODIUM_EXPORT +const char *crypto_secretbox_primitive(void); + +#define crypto_secretbox_MESSAGEBYTES_MAX crypto_secretbox_xsalsa20poly1305_MESSAGEBYTES_MAX +SODIUM_EXPORT +size_t crypto_secretbox_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_secretbox_easy(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_secretbox_open_easy(unsigned char *m, const unsigned char *c, + unsigned long long clen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +SODIUM_EXPORT +int crypto_secretbox_detached(unsigned char *c, unsigned char *mac, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 2, 5, 6))); + +SODIUM_EXPORT +int crypto_secretbox_open_detached(unsigned char *m, + const unsigned char *c, + const unsigned char *mac, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6))); + +SODIUM_EXPORT +void crypto_secretbox_keygen(unsigned char k[crypto_secretbox_KEYBYTES]) + __attribute__ ((nonnull)); + +/* -- NaCl compatibility interface ; Requires padding -- */ + +#define crypto_secretbox_ZEROBYTES crypto_secretbox_xsalsa20poly1305_ZEROBYTES +SODIUM_EXPORT +size_t crypto_secretbox_zerobytes(void); + +#define crypto_secretbox_BOXZEROBYTES crypto_secretbox_xsalsa20poly1305_BOXZEROBYTES +SODIUM_EXPORT +size_t crypto_secretbox_boxzerobytes(void); + +SODIUM_EXPORT +int crypto_secretbox(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_secretbox_open(unsigned char *m, const unsigned char *c, + unsigned long long clen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xchacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xchacha20poly1305.h new file mode 100644 index 0000000000..6ec674e310 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xchacha20poly1305.h @@ -0,0 +1,70 @@ +#ifndef crypto_secretbox_xchacha20poly1305_H +#define crypto_secretbox_xchacha20poly1305_H + +#include +#include "crypto_stream_xchacha20.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_secretbox_xchacha20poly1305_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_secretbox_xchacha20poly1305_keybytes(void); + +#define crypto_secretbox_xchacha20poly1305_NONCEBYTES 24U +SODIUM_EXPORT +size_t crypto_secretbox_xchacha20poly1305_noncebytes(void); + +#define crypto_secretbox_xchacha20poly1305_MACBYTES 16U +SODIUM_EXPORT +size_t crypto_secretbox_xchacha20poly1305_macbytes(void); + +#define crypto_secretbox_xchacha20poly1305_MESSAGEBYTES_MAX \ + (crypto_stream_xchacha20_MESSAGEBYTES_MAX - crypto_secretbox_xchacha20poly1305_MACBYTES) +SODIUM_EXPORT +size_t crypto_secretbox_xchacha20poly1305_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_secretbox_xchacha20poly1305_easy(unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_secretbox_xchacha20poly1305_open_easy(unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +SODIUM_EXPORT +int crypto_secretbox_xchacha20poly1305_detached(unsigned char *c, + unsigned char *mac, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 2, 5, 6))); + +SODIUM_EXPORT +int crypto_secretbox_xchacha20poly1305_open_detached(unsigned char *m, + const unsigned char *c, + const unsigned char *mac, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6))); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xsalsa20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xsalsa20poly1305.h new file mode 100644 index 0000000000..be0874cbaf --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xsalsa20poly1305.h @@ -0,0 +1,69 @@ +#ifndef crypto_secretbox_xsalsa20poly1305_H +#define crypto_secretbox_xsalsa20poly1305_H + +#include +#include "crypto_stream_xsalsa20.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_secretbox_xsalsa20poly1305_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_secretbox_xsalsa20poly1305_keybytes(void); + +#define crypto_secretbox_xsalsa20poly1305_NONCEBYTES 24U +SODIUM_EXPORT +size_t crypto_secretbox_xsalsa20poly1305_noncebytes(void); + +#define crypto_secretbox_xsalsa20poly1305_MACBYTES 16U +SODIUM_EXPORT +size_t crypto_secretbox_xsalsa20poly1305_macbytes(void); + +/* Only for the libsodium API - The NaCl compatibility API would require BOXZEROBYTES extra bytes */ +#define crypto_secretbox_xsalsa20poly1305_MESSAGEBYTES_MAX \ + (crypto_stream_xsalsa20_MESSAGEBYTES_MAX - crypto_secretbox_xsalsa20poly1305_MACBYTES) +SODIUM_EXPORT +size_t crypto_secretbox_xsalsa20poly1305_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_secretbox_xsalsa20poly1305(unsigned char *c, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_secretbox_xsalsa20poly1305_open(unsigned char *m, + const unsigned char *c, + unsigned long long clen, + const unsigned char *n, + const unsigned char *k) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); + +SODIUM_EXPORT +void crypto_secretbox_xsalsa20poly1305_keygen(unsigned char k[crypto_secretbox_xsalsa20poly1305_KEYBYTES]) + __attribute__ ((nonnull)); + +/* -- NaCl compatibility interface ; Requires padding -- */ + +#define crypto_secretbox_xsalsa20poly1305_BOXZEROBYTES 16U +SODIUM_EXPORT +size_t crypto_secretbox_xsalsa20poly1305_boxzerobytes(void); + +#define crypto_secretbox_xsalsa20poly1305_ZEROBYTES \ + (crypto_secretbox_xsalsa20poly1305_BOXZEROBYTES + \ + crypto_secretbox_xsalsa20poly1305_MACBYTES) +SODIUM_EXPORT +size_t crypto_secretbox_xsalsa20poly1305_zerobytes(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretstream_xchacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretstream_xchacha20poly1305.h new file mode 100644 index 0000000000..b22e4e9313 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretstream_xchacha20poly1305.h @@ -0,0 +1,108 @@ +#ifndef crypto_secretstream_xchacha20poly1305_H +#define crypto_secretstream_xchacha20poly1305_H + +#include + +#include "crypto_aead_xchacha20poly1305.h" +#include "crypto_stream_chacha20.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_secretstream_xchacha20poly1305_ABYTES \ + (1U + crypto_aead_xchacha20poly1305_ietf_ABYTES) +SODIUM_EXPORT +size_t crypto_secretstream_xchacha20poly1305_abytes(void); + +#define crypto_secretstream_xchacha20poly1305_HEADERBYTES \ + crypto_aead_xchacha20poly1305_ietf_NPUBBYTES +SODIUM_EXPORT +size_t crypto_secretstream_xchacha20poly1305_headerbytes(void); + +#define crypto_secretstream_xchacha20poly1305_KEYBYTES \ + crypto_aead_xchacha20poly1305_ietf_KEYBYTES +SODIUM_EXPORT +size_t crypto_secretstream_xchacha20poly1305_keybytes(void); + +#define crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX \ + SODIUM_MIN(SODIUM_SIZE_MAX - crypto_secretstream_xchacha20poly1305_ABYTES, \ + (64ULL * ((1ULL << 32) - 2ULL))) +SODIUM_EXPORT +size_t crypto_secretstream_xchacha20poly1305_messagebytes_max(void); + +#define crypto_secretstream_xchacha20poly1305_TAG_MESSAGE 0x00 +SODIUM_EXPORT +unsigned char crypto_secretstream_xchacha20poly1305_tag_message(void); + +#define crypto_secretstream_xchacha20poly1305_TAG_PUSH 0x01 +SODIUM_EXPORT +unsigned char crypto_secretstream_xchacha20poly1305_tag_push(void); + +#define crypto_secretstream_xchacha20poly1305_TAG_REKEY 0x02 +SODIUM_EXPORT +unsigned char crypto_secretstream_xchacha20poly1305_tag_rekey(void); + +#define crypto_secretstream_xchacha20poly1305_TAG_FINAL \ + (crypto_secretstream_xchacha20poly1305_TAG_PUSH | \ + crypto_secretstream_xchacha20poly1305_TAG_REKEY) +SODIUM_EXPORT +unsigned char crypto_secretstream_xchacha20poly1305_tag_final(void); + +typedef struct crypto_secretstream_xchacha20poly1305_state { + unsigned char k[crypto_stream_chacha20_ietf_KEYBYTES]; + unsigned char nonce[crypto_stream_chacha20_ietf_NONCEBYTES]; + unsigned char _pad[8]; +} crypto_secretstream_xchacha20poly1305_state; + +SODIUM_EXPORT +size_t crypto_secretstream_xchacha20poly1305_statebytes(void); + +SODIUM_EXPORT +void crypto_secretstream_xchacha20poly1305_keygen + (unsigned char k[crypto_secretstream_xchacha20poly1305_KEYBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_secretstream_xchacha20poly1305_init_push + (crypto_secretstream_xchacha20poly1305_state *state, + unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES], + const unsigned char k[crypto_secretstream_xchacha20poly1305_KEYBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_secretstream_xchacha20poly1305_push + (crypto_secretstream_xchacha20poly1305_state *state, + unsigned char *c, unsigned long long *clen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *ad, unsigned long long adlen, unsigned char tag) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_secretstream_xchacha20poly1305_init_pull + (crypto_secretstream_xchacha20poly1305_state *state, + const unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES], + const unsigned char k[crypto_secretstream_xchacha20poly1305_KEYBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_secretstream_xchacha20poly1305_pull + (crypto_secretstream_xchacha20poly1305_state *state, + unsigned char *m, unsigned long long *mlen_p, unsigned char *tag_p, + const unsigned char *c, unsigned long long clen, + const unsigned char *ad, unsigned long long adlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +void crypto_secretstream_xchacha20poly1305_rekey + (crypto_secretstream_xchacha20poly1305_state *state); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash.h new file mode 100644 index 0000000000..fecaa88bd8 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash.h @@ -0,0 +1,41 @@ +#ifndef crypto_shorthash_H +#define crypto_shorthash_H + +#include + +#include "crypto_shorthash_siphash24.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_shorthash_BYTES crypto_shorthash_siphash24_BYTES +SODIUM_EXPORT +size_t crypto_shorthash_bytes(void); + +#define crypto_shorthash_KEYBYTES crypto_shorthash_siphash24_KEYBYTES +SODIUM_EXPORT +size_t crypto_shorthash_keybytes(void); + +#define crypto_shorthash_PRIMITIVE "siphash24" +SODIUM_EXPORT +const char *crypto_shorthash_primitive(void); + +SODIUM_EXPORT +int crypto_shorthash(unsigned char *out, const unsigned char *in, + unsigned long long inlen, const unsigned char *k) + __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +void crypto_shorthash_keygen(unsigned char k[crypto_shorthash_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash_siphash24.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash_siphash24.h new file mode 100644 index 0000000000..1e6f72a620 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash_siphash24.h @@ -0,0 +1,50 @@ +#ifndef crypto_shorthash_siphash24_H +#define crypto_shorthash_siphash24_H + +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +/* -- 64-bit output -- */ + +#define crypto_shorthash_siphash24_BYTES 8U +SODIUM_EXPORT +size_t crypto_shorthash_siphash24_bytes(void); + +#define crypto_shorthash_siphash24_KEYBYTES 16U +SODIUM_EXPORT +size_t crypto_shorthash_siphash24_keybytes(void); + +SODIUM_EXPORT +int crypto_shorthash_siphash24(unsigned char *out, const unsigned char *in, + unsigned long long inlen, const unsigned char *k) + __attribute__ ((nonnull(1, 4))); + +#ifndef SODIUM_LIBRARY_MINIMAL +/* -- 128-bit output -- */ + +#define crypto_shorthash_siphashx24_BYTES 16U +SODIUM_EXPORT +size_t crypto_shorthash_siphashx24_bytes(void); + +#define crypto_shorthash_siphashx24_KEYBYTES 16U +SODIUM_EXPORT +size_t crypto_shorthash_siphashx24_keybytes(void); + +SODIUM_EXPORT +int crypto_shorthash_siphashx24(unsigned char *out, const unsigned char *in, + unsigned long long inlen, const unsigned char *k) + __attribute__ ((nonnull(1, 4))); +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign.h new file mode 100644 index 0000000000..f5fafb123e --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign.h @@ -0,0 +1,107 @@ +#ifndef crypto_sign_H +#define crypto_sign_H + +/* + * THREAD SAFETY: crypto_sign_keypair() is thread-safe, + * provided that sodium_init() was called before. + * + * Other functions, including crypto_sign_seed_keypair() are always thread-safe. + */ + +#include + +#include "crypto_sign_ed25519.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef crypto_sign_ed25519ph_state crypto_sign_state; + +SODIUM_EXPORT +size_t crypto_sign_statebytes(void); + +#define crypto_sign_BYTES crypto_sign_ed25519_BYTES +SODIUM_EXPORT +size_t crypto_sign_bytes(void); + +#define crypto_sign_SEEDBYTES crypto_sign_ed25519_SEEDBYTES +SODIUM_EXPORT +size_t crypto_sign_seedbytes(void); + +#define crypto_sign_PUBLICKEYBYTES crypto_sign_ed25519_PUBLICKEYBYTES +SODIUM_EXPORT +size_t crypto_sign_publickeybytes(void); + +#define crypto_sign_SECRETKEYBYTES crypto_sign_ed25519_SECRETKEYBYTES +SODIUM_EXPORT +size_t crypto_sign_secretkeybytes(void); + +#define crypto_sign_MESSAGEBYTES_MAX crypto_sign_ed25519_MESSAGEBYTES_MAX +SODIUM_EXPORT +size_t crypto_sign_messagebytes_max(void); + +#define crypto_sign_PRIMITIVE "ed25519" +SODIUM_EXPORT +const char *crypto_sign_primitive(void); + +SODIUM_EXPORT +int crypto_sign_seed_keypair(unsigned char *pk, unsigned char *sk, + const unsigned char *seed) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_keypair(unsigned char *pk, unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign(unsigned char *sm, unsigned long long *smlen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_open(unsigned char *m, unsigned long long *mlen_p, + const unsigned char *sm, unsigned long long smlen, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5))); + +SODIUM_EXPORT +int crypto_sign_detached(unsigned char *sig, unsigned long long *siglen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_sign_init(crypto_sign_state *state); + +SODIUM_EXPORT +int crypto_sign_update(crypto_sign_state *state, + const unsigned char *m, unsigned long long mlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_sign_final_create(crypto_sign_state *state, unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *sk) + __attribute__ ((nonnull(1, 2, 4))); + +SODIUM_EXPORT +int crypto_sign_final_verify(crypto_sign_state *state, const unsigned char *sig, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_ed25519.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_ed25519.h new file mode 100644 index 0000000000..0fdac42d35 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_ed25519.h @@ -0,0 +1,124 @@ +#ifndef crypto_sign_ed25519_H +#define crypto_sign_ed25519_H + +#include +#include "crypto_hash_sha512.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct crypto_sign_ed25519ph_state { + crypto_hash_sha512_state hs; +} crypto_sign_ed25519ph_state; + +SODIUM_EXPORT +size_t crypto_sign_ed25519ph_statebytes(void); + +#define crypto_sign_ed25519_BYTES 64U +SODIUM_EXPORT +size_t crypto_sign_ed25519_bytes(void); + +#define crypto_sign_ed25519_SEEDBYTES 32U +SODIUM_EXPORT +size_t crypto_sign_ed25519_seedbytes(void); + +#define crypto_sign_ed25519_PUBLICKEYBYTES 32U +SODIUM_EXPORT +size_t crypto_sign_ed25519_publickeybytes(void); + +#define crypto_sign_ed25519_SECRETKEYBYTES (32U + 32U) +SODIUM_EXPORT +size_t crypto_sign_ed25519_secretkeybytes(void); + +#define crypto_sign_ed25519_MESSAGEBYTES_MAX (SODIUM_SIZE_MAX - crypto_sign_ed25519_BYTES) +SODIUM_EXPORT +size_t crypto_sign_ed25519_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_sign_ed25519(unsigned char *sm, unsigned long long *smlen_p, + const unsigned char *m, unsigned long long mlen, + const unsigned char *sk) + __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_open(unsigned char *m, unsigned long long *mlen_p, + const unsigned char *sm, unsigned long long smlen, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_detached(unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *sk) + __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_ed25519_verify_detached(const unsigned char *sig, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); + +SODIUM_EXPORT +int crypto_sign_ed25519_keypair(unsigned char *pk, unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_seed_keypair(unsigned char *pk, unsigned char *sk, + const unsigned char *seed) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_pk_to_curve25519(unsigned char *curve25519_pk, + const unsigned char *ed25519_pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_curve25519(unsigned char *curve25519_sk, + const unsigned char *ed25519_sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_seed(unsigned char *seed, + const unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519_sk_to_pk(unsigned char *pk, const unsigned char *sk) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_init(crypto_sign_ed25519ph_state *state) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_update(crypto_sign_ed25519ph_state *state, + const unsigned char *m, + unsigned long long mlen) + __attribute__ ((nonnull(1))); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_final_create(crypto_sign_ed25519ph_state *state, + unsigned char *sig, + unsigned long long *siglen_p, + const unsigned char *sk) + __attribute__ ((nonnull(1, 2, 4))); + +SODIUM_EXPORT +int crypto_sign_ed25519ph_final_verify(crypto_sign_ed25519ph_state *state, + const unsigned char *sig, + const unsigned char *pk) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_edwards25519sha512batch.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_edwards25519sha512batch.h new file mode 100644 index 0000000000..eed158aa84 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_edwards25519sha512batch.h @@ -0,0 +1,55 @@ +#ifndef crypto_sign_edwards25519sha512batch_H +#define crypto_sign_edwards25519sha512batch_H + +/* + * WARNING: This construction was a prototype, which should not be used + * any more in new projects. + * + * crypto_sign_edwards25519sha512batch is provided for applications + * initially built with NaCl, but as recommended by the author of this + * construction, new applications should use ed25519 instead. + * + * In Sodium, you should use the high-level crypto_sign_*() functions instead. + */ + +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_sign_edwards25519sha512batch_BYTES 64U +#define crypto_sign_edwards25519sha512batch_PUBLICKEYBYTES 32U +#define crypto_sign_edwards25519sha512batch_SECRETKEYBYTES (32U + 32U) +#define crypto_sign_edwards25519sha512batch_MESSAGEBYTES_MAX (SODIUM_SIZE_MAX - crypto_sign_edwards25519sha512batch_BYTES) + +SODIUM_EXPORT +int crypto_sign_edwards25519sha512batch(unsigned char *sm, + unsigned long long *smlen_p, + const unsigned char *m, + unsigned long long mlen, + const unsigned char *sk) + __attribute__ ((deprecated)) __attribute__ ((nonnull(1, 5))); + +SODIUM_EXPORT +int crypto_sign_edwards25519sha512batch_open(unsigned char *m, + unsigned long long *mlen_p, + const unsigned char *sm, + unsigned long long smlen, + const unsigned char *pk) + __attribute__ ((deprecated)) __attribute__ ((nonnull(3, 5))); + +SODIUM_EXPORT +int crypto_sign_edwards25519sha512batch_keypair(unsigned char *pk, + unsigned char *sk) + __attribute__ ((deprecated)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream.h new file mode 100644 index 0000000000..9f3b57f5cc --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream.h @@ -0,0 +1,59 @@ +#ifndef crypto_stream_H +#define crypto_stream_H + +/* + * WARNING: This is just a stream cipher. It is NOT authenticated encryption. + * While it provides some protection against eavesdropping, it does NOT + * provide any security against active attacks. + * Unless you know what you're doing, what you are looking for is probably + * the crypto_box functions. + */ + +#include + +#include "crypto_stream_xsalsa20.h" +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_stream_KEYBYTES crypto_stream_xsalsa20_KEYBYTES +SODIUM_EXPORT +size_t crypto_stream_keybytes(void); + +#define crypto_stream_NONCEBYTES crypto_stream_xsalsa20_NONCEBYTES +SODIUM_EXPORT +size_t crypto_stream_noncebytes(void); + +#define crypto_stream_MESSAGEBYTES_MAX crypto_stream_xsalsa20_MESSAGEBYTES_MAX +SODIUM_EXPORT +size_t crypto_stream_messagebytes_max(void); + +#define crypto_stream_PRIMITIVE "xsalsa20" +SODIUM_EXPORT +const char *crypto_stream_primitive(void); + +SODIUM_EXPORT +int crypto_stream(unsigned char *c, unsigned long long clen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_stream_xor(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +void crypto_stream_keygen(unsigned char k[crypto_stream_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_chacha20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_chacha20.h new file mode 100644 index 0000000000..cb034f3631 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_chacha20.h @@ -0,0 +1,106 @@ +#ifndef crypto_stream_chacha20_H +#define crypto_stream_chacha20_H + +/* + * WARNING: This is just a stream cipher. It is NOT authenticated encryption. + * While it provides some protection against eavesdropping, it does NOT + * provide any security against active attacks. + * Unless you know what you're doing, what you are looking for is probably + * the crypto_box functions. + */ + +#include +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_stream_chacha20_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_stream_chacha20_keybytes(void); + +#define crypto_stream_chacha20_NONCEBYTES 8U +SODIUM_EXPORT +size_t crypto_stream_chacha20_noncebytes(void); + +#define crypto_stream_chacha20_MESSAGEBYTES_MAX SODIUM_SIZE_MAX +SODIUM_EXPORT +size_t crypto_stream_chacha20_messagebytes_max(void); + +/* ChaCha20 with a 64-bit nonce and a 64-bit counter, as originally designed */ + +SODIUM_EXPORT +int crypto_stream_chacha20(unsigned char *c, unsigned long long clen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_stream_chacha20_xor(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_stream_chacha20_xor_ic(unsigned char *c, const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, uint64_t ic, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 6))); + +SODIUM_EXPORT +void crypto_stream_chacha20_keygen(unsigned char k[crypto_stream_chacha20_KEYBYTES]) + __attribute__ ((nonnull)); + +/* ChaCha20 with a 96-bit nonce and a 32-bit counter (IETF) */ + +#define crypto_stream_chacha20_ietf_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_stream_chacha20_ietf_keybytes(void); + +#define crypto_stream_chacha20_ietf_NONCEBYTES 12U +SODIUM_EXPORT +size_t crypto_stream_chacha20_ietf_noncebytes(void); + +#define crypto_stream_chacha20_ietf_MESSAGEBYTES_MAX \ + SODIUM_MIN(SODIUM_SIZE_MAX, 64ULL * (1ULL << 32)) +SODIUM_EXPORT +size_t crypto_stream_chacha20_ietf_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_stream_chacha20_ietf(unsigned char *c, unsigned long long clen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_stream_chacha20_ietf_xor(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_stream_chacha20_ietf_xor_ic(unsigned char *c, const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, uint32_t ic, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 6))); + +SODIUM_EXPORT +void crypto_stream_chacha20_ietf_keygen(unsigned char k[crypto_stream_chacha20_ietf_KEYBYTES]) + __attribute__ ((nonnull)); + +/* Aliases */ + +#define crypto_stream_chacha20_IETF_KEYBYTES crypto_stream_chacha20_ietf_KEYBYTES +#define crypto_stream_chacha20_IETF_NONCEBYTES crypto_stream_chacha20_ietf_NONCEBYTES +#define crypto_stream_chacha20_IETF_MESSAGEBYTES_MAX crypto_stream_chacha20_ietf_MESSAGEBYTES_MAX + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa20.h new file mode 100644 index 0000000000..27cc007e57 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa20.h @@ -0,0 +1,61 @@ +#ifndef crypto_stream_salsa20_H +#define crypto_stream_salsa20_H + +/* + * WARNING: This is just a stream cipher. It is NOT authenticated encryption. + * While it provides some protection against eavesdropping, it does NOT + * provide any security against active attacks. + * Unless you know what you're doing, what you are looking for is probably + * the crypto_box functions. + */ + +#include +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_stream_salsa20_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_stream_salsa20_keybytes(void); + +#define crypto_stream_salsa20_NONCEBYTES 8U +SODIUM_EXPORT +size_t crypto_stream_salsa20_noncebytes(void); + +#define crypto_stream_salsa20_MESSAGEBYTES_MAX SODIUM_SIZE_MAX +SODIUM_EXPORT +size_t crypto_stream_salsa20_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_stream_salsa20(unsigned char *c, unsigned long long clen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_stream_salsa20_xor(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_stream_salsa20_xor_ic(unsigned char *c, const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, uint64_t ic, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 6))); + +SODIUM_EXPORT +void crypto_stream_salsa20_keygen(unsigned char k[crypto_stream_salsa20_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa2012.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa2012.h new file mode 100644 index 0000000000..f1ec382555 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa2012.h @@ -0,0 +1,53 @@ +#ifndef crypto_stream_salsa2012_H +#define crypto_stream_salsa2012_H + +/* + * WARNING: This is just a stream cipher. It is NOT authenticated encryption. + * While it provides some protection against eavesdropping, it does NOT + * provide any security against active attacks. + * Unless you know what you're doing, what you are looking for is probably + * the crypto_box functions. + */ + +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_stream_salsa2012_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_stream_salsa2012_keybytes(void); + +#define crypto_stream_salsa2012_NONCEBYTES 8U +SODIUM_EXPORT +size_t crypto_stream_salsa2012_noncebytes(void); + +#define crypto_stream_salsa2012_MESSAGEBYTES_MAX SODIUM_SIZE_MAX +SODIUM_EXPORT +size_t crypto_stream_salsa2012_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_stream_salsa2012(unsigned char *c, unsigned long long clen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_stream_salsa2012_xor(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +void crypto_stream_salsa2012_keygen(unsigned char k[crypto_stream_salsa2012_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa208.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa208.h new file mode 100644 index 0000000000..a7169542e7 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa208.h @@ -0,0 +1,56 @@ +#ifndef crypto_stream_salsa208_H +#define crypto_stream_salsa208_H + +/* + * WARNING: This is just a stream cipher. It is NOT authenticated encryption. + * While it provides some protection against eavesdropping, it does NOT + * provide any security against active attacks. + * Unless you know what you're doing, what you are looking for is probably + * the crypto_box functions. + */ + +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_stream_salsa208_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_stream_salsa208_keybytes(void) + __attribute__ ((deprecated)); + +#define crypto_stream_salsa208_NONCEBYTES 8U +SODIUM_EXPORT +size_t crypto_stream_salsa208_noncebytes(void) + __attribute__ ((deprecated)); + +#define crypto_stream_salsa208_MESSAGEBYTES_MAX SODIUM_SIZE_MAX + SODIUM_EXPORT +size_t crypto_stream_salsa208_messagebytes_max(void) + __attribute__ ((deprecated)); + +SODIUM_EXPORT +int crypto_stream_salsa208(unsigned char *c, unsigned long long clen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((deprecated)) __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_stream_salsa208_xor(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((deprecated)) __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +void crypto_stream_salsa208_keygen(unsigned char k[crypto_stream_salsa208_KEYBYTES]) + __attribute__ ((deprecated)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xchacha20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xchacha20.h new file mode 100644 index 0000000000..06f9908a11 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xchacha20.h @@ -0,0 +1,61 @@ +#ifndef crypto_stream_xchacha20_H +#define crypto_stream_xchacha20_H + +/* + * WARNING: This is just a stream cipher. It is NOT authenticated encryption. + * While it provides some protection against eavesdropping, it does NOT + * provide any security against active attacks. + * Unless you know what you're doing, what you are looking for is probably + * the crypto_box functions. + */ + +#include +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_stream_xchacha20_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_stream_xchacha20_keybytes(void); + +#define crypto_stream_xchacha20_NONCEBYTES 24U +SODIUM_EXPORT +size_t crypto_stream_xchacha20_noncebytes(void); + +#define crypto_stream_xchacha20_MESSAGEBYTES_MAX SODIUM_SIZE_MAX +SODIUM_EXPORT +size_t crypto_stream_xchacha20_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_stream_xchacha20(unsigned char *c, unsigned long long clen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_stream_xchacha20_xor(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_stream_xchacha20_xor_ic(unsigned char *c, const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, uint64_t ic, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 6))); + +SODIUM_EXPORT +void crypto_stream_xchacha20_keygen(unsigned char k[crypto_stream_xchacha20_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xsalsa20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xsalsa20.h new file mode 100644 index 0000000000..71d1675df3 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xsalsa20.h @@ -0,0 +1,61 @@ +#ifndef crypto_stream_xsalsa20_H +#define crypto_stream_xsalsa20_H + +/* + * WARNING: This is just a stream cipher. It is NOT authenticated encryption. + * While it provides some protection against eavesdropping, it does NOT + * provide any security against active attacks. + * Unless you know what you're doing, what you are looking for is probably + * the crypto_box functions. + */ + +#include +#include +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_stream_xsalsa20_KEYBYTES 32U +SODIUM_EXPORT +size_t crypto_stream_xsalsa20_keybytes(void); + +#define crypto_stream_xsalsa20_NONCEBYTES 24U +SODIUM_EXPORT +size_t crypto_stream_xsalsa20_noncebytes(void); + +#define crypto_stream_xsalsa20_MESSAGEBYTES_MAX SODIUM_SIZE_MAX +SODIUM_EXPORT +size_t crypto_stream_xsalsa20_messagebytes_max(void); + +SODIUM_EXPORT +int crypto_stream_xsalsa20(unsigned char *c, unsigned long long clen, + const unsigned char *n, const unsigned char *k) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +int crypto_stream_xsalsa20_xor(unsigned char *c, const unsigned char *m, + unsigned long long mlen, const unsigned char *n, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 5))); + +SODIUM_EXPORT +int crypto_stream_xsalsa20_xor_ic(unsigned char *c, const unsigned char *m, + unsigned long long mlen, + const unsigned char *n, uint64_t ic, + const unsigned char *k) + __attribute__ ((nonnull(1, 4, 6))); + +SODIUM_EXPORT +void crypto_stream_xsalsa20_keygen(unsigned char k[crypto_stream_xsalsa20_KEYBYTES]) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_16.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_16.h new file mode 100644 index 0000000000..7b9c8077ad --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_16.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_16_H +#define crypto_verify_16_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_16_BYTES 16U +SODIUM_EXPORT +size_t crypto_verify_16_bytes(void); + +SODIUM_EXPORT +int crypto_verify_16(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_32.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_32.h new file mode 100644 index 0000000000..9b0f4529f6 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_32.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_32_H +#define crypto_verify_32_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_32_BYTES 32U +SODIUM_EXPORT +size_t crypto_verify_32_bytes(void); + +SODIUM_EXPORT +int crypto_verify_32(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_64.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_64.h new file mode 100644 index 0000000000..c83b73025a --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_64.h @@ -0,0 +1,23 @@ +#ifndef crypto_verify_64_H +#define crypto_verify_64_H + +#include +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_verify_64_BYTES 64U +SODIUM_EXPORT +size_t crypto_verify_64_bytes(void); + +SODIUM_EXPORT +int crypto_verify_64(const unsigned char *x, const unsigned char *y) + __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake128.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake128.h new file mode 100644 index 0000000000..aed16a77f5 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake128.h @@ -0,0 +1,55 @@ +#ifndef crypto_xof_shake128_H +#define crypto_xof_shake128_H + +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +#define crypto_xof_shake128_BLOCKBYTES 168U +SODIUM_EXPORT +size_t crypto_xof_shake128_blockbytes(void); + +#define crypto_xof_shake128_STATEBYTES 256U +SODIUM_EXPORT +size_t crypto_xof_shake128_statebytes(void); + +#define crypto_xof_shake128_DOMAIN_STANDARD 0x1FU +SODIUM_EXPORT +unsigned char crypto_xof_shake128_domain_standard(void); + +typedef struct CRYPTO_ALIGN(16) crypto_xof_shake128_state { + unsigned char opaque[256]; +} crypto_xof_shake128_state; + +SODIUM_EXPORT +int crypto_xof_shake128(unsigned char *out, size_t outlen, const unsigned char *in, + unsigned long long inlen) __attribute__((nonnull(1))); + +SODIUM_EXPORT +int crypto_xof_shake128_init(crypto_xof_shake128_state *state) __attribute__((nonnull)); + +SODIUM_EXPORT +int crypto_xof_shake128_init_with_domain(crypto_xof_shake128_state *state, unsigned char domain) + __attribute__((nonnull)); + +SODIUM_EXPORT +int crypto_xof_shake128_update(crypto_xof_shake128_state *state, + const unsigned char *in, + unsigned long long inlen) __attribute__((nonnull(1))); + +SODIUM_EXPORT +int crypto_xof_shake128_squeeze(crypto_xof_shake128_state *state, unsigned char *out, size_t outlen) + __attribute__((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake256.h new file mode 100644 index 0000000000..9f4bfc7243 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake256.h @@ -0,0 +1,54 @@ +#ifndef crypto_xof_shake256_H +#define crypto_xof_shake256_H + +#include "export.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_xof_shake256_BLOCKBYTES 136U +SODIUM_EXPORT +size_t crypto_xof_shake256_blockbytes(void); + +#define crypto_xof_shake256_STATEBYTES 256U +SODIUM_EXPORT +size_t crypto_xof_shake256_statebytes(void); + +#define crypto_xof_shake256_DOMAIN_STANDARD 0x1FU +SODIUM_EXPORT +unsigned char crypto_xof_shake256_domain_standard(void); + +typedef struct CRYPTO_ALIGN(16) crypto_xof_shake256_state { + unsigned char opaque[256]; +} crypto_xof_shake256_state; + +/* One-shot API */ +SODIUM_EXPORT +int crypto_xof_shake256(unsigned char *out, size_t outlen, const unsigned char *in, + unsigned long long inlen) __attribute__((nonnull(1))); + +/* Streaming API with standard domain */ +SODIUM_EXPORT +int crypto_xof_shake256_init(crypto_xof_shake256_state *state) __attribute__((nonnull)); + +/* Streaming API with custom domain */ +SODIUM_EXPORT +int crypto_xof_shake256_init_with_domain(crypto_xof_shake256_state *state, unsigned char domain) + __attribute__((nonnull)); + +SODIUM_EXPORT +int crypto_xof_shake256_update(crypto_xof_shake256_state *state, + const unsigned char *in, + unsigned long long inlen) __attribute__((nonnull(1))); + +SODIUM_EXPORT +int crypto_xof_shake256_squeeze(crypto_xof_shake256_state *state, unsigned char *out, size_t outlen) + __attribute__((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake128.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake128.h new file mode 100644 index 0000000000..f8c3e30dd4 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake128.h @@ -0,0 +1,54 @@ +#ifndef crypto_xof_turboshake128_H +#define crypto_xof_turboshake128_H + +#include "export.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_xof_turboshake128_BLOCKBYTES 168U +SODIUM_EXPORT +size_t crypto_xof_turboshake128_blockbytes(void); + +#define crypto_xof_turboshake128_STATEBYTES 256U +SODIUM_EXPORT +size_t crypto_xof_turboshake128_statebytes(void); + +#define crypto_xof_turboshake128_DOMAIN_STANDARD 0x1FU +SODIUM_EXPORT +unsigned char crypto_xof_turboshake128_domain_standard(void); + +typedef struct CRYPTO_ALIGN(16) crypto_xof_turboshake128_state { + unsigned char opaque[256]; +} crypto_xof_turboshake128_state; + +/* One-shot API */ +SODIUM_EXPORT +int crypto_xof_turboshake128(unsigned char *out, size_t outlen, const unsigned char *in, + unsigned long long inlen) __attribute__((nonnull(1))); + +/* Streaming API with standard domain */ +SODIUM_EXPORT +int crypto_xof_turboshake128_init(crypto_xof_turboshake128_state *state) __attribute__((nonnull)); + +/* Streaming API with custom domain */ +SODIUM_EXPORT +int crypto_xof_turboshake128_init_with_domain(crypto_xof_turboshake128_state *state, + unsigned char domain) __attribute__((nonnull)); + +SODIUM_EXPORT +int crypto_xof_turboshake128_update(crypto_xof_turboshake128_state *state, + const unsigned char *in, + unsigned long long inlen) __attribute__((nonnull(1))); + +SODIUM_EXPORT +int crypto_xof_turboshake128_squeeze(crypto_xof_turboshake128_state *state, unsigned char *out, + size_t outlen) __attribute__((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake256.h new file mode 100644 index 0000000000..b325067d10 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake256.h @@ -0,0 +1,54 @@ +#ifndef crypto_xof_turboshake256_H +#define crypto_xof_turboshake256_H + +#include "export.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define crypto_xof_turboshake256_BLOCKBYTES 136U +SODIUM_EXPORT +size_t crypto_xof_turboshake256_blockbytes(void); + +#define crypto_xof_turboshake256_STATEBYTES 256U +SODIUM_EXPORT +size_t crypto_xof_turboshake256_statebytes(void); + +#define crypto_xof_turboshake256_DOMAIN_STANDARD 0x1FU +SODIUM_EXPORT +unsigned char crypto_xof_turboshake256_domain_standard(void); + +typedef struct CRYPTO_ALIGN(16) crypto_xof_turboshake256_state { + unsigned char opaque[256]; +} crypto_xof_turboshake256_state; + +/* One-shot API */ +SODIUM_EXPORT +int crypto_xof_turboshake256(unsigned char *out, size_t outlen, const unsigned char *in, + unsigned long long inlen) __attribute__((nonnull(1))); + +/* Streaming API with standard domain */ +SODIUM_EXPORT +int crypto_xof_turboshake256_init(crypto_xof_turboshake256_state *state) __attribute__((nonnull)); + +/* Streaming API with custom domain */ +SODIUM_EXPORT +int crypto_xof_turboshake256_init_with_domain(crypto_xof_turboshake256_state *state, + unsigned char domain) __attribute__((nonnull)); + +SODIUM_EXPORT +int crypto_xof_turboshake256_update(crypto_xof_turboshake256_state *state, + const unsigned char *in, + unsigned long long inlen) __attribute__((nonnull(1))); + +SODIUM_EXPORT +int crypto_xof_turboshake256_squeeze(crypto_xof_turboshake256_state *state, unsigned char *out, + size_t outlen) __attribute__((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/export.h b/quartz/src/nativeInterop/libsodium/include/sodium/export.h new file mode 100644 index 0000000000..a0074fc9cb --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/export.h @@ -0,0 +1,57 @@ + +#ifndef sodium_export_H +#define sodium_export_H + +#include +#include +#include + +#if !defined(__clang__) && !defined(__GNUC__) +# ifdef __attribute__ +# undef __attribute__ +# endif +# define __attribute__(a) +#endif + +#ifdef SODIUM_STATIC +# define SODIUM_EXPORT +# define SODIUM_EXPORT_WEAK +#else +# if defined(_MSC_VER) +# ifdef SODIUM_DLL_EXPORT +# define SODIUM_EXPORT __declspec(dllexport) +# else +# define SODIUM_EXPORT __declspec(dllimport) +# endif +# else +# if defined(__SUNPRO_C) +# ifndef __GNU_C__ +# define SODIUM_EXPORT __attribute__ (visibility(__global)) +# else +# define SODIUM_EXPORT __attribute__ __global +# endif +# elif defined(_MSG_VER) +# define SODIUM_EXPORT extern __declspec(dllexport) +# else +# define SODIUM_EXPORT __attribute__ ((visibility ("default"))) +# endif +# endif +# if defined(__ELF__) && !defined(SODIUM_DISABLE_WEAK_FUNCTIONS) +# define SODIUM_EXPORT_WEAK SODIUM_EXPORT __attribute__((weak)) +# else +# define SODIUM_EXPORT_WEAK SODIUM_EXPORT +# endif +#endif + +#ifndef CRYPTO_ALIGN +# if defined(__INTEL_COMPILER) || defined(_MSC_VER) +# define CRYPTO_ALIGN(x) __declspec(align(x)) +# else +# define CRYPTO_ALIGN(x) __attribute__ ((aligned(x))) +# endif +#endif + +#define SODIUM_MIN(A, B) ((A) < (B) ? (A) : (B)) +#define SODIUM_SIZE_MAX SODIUM_MIN(UINT64_MAX, SIZE_MAX) + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes.h b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes.h new file mode 100644 index 0000000000..c83a4df50b --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes.h @@ -0,0 +1,72 @@ + +#ifndef randombytes_H +#define randombytes_H + +#include +#include + +#include + +#include "export.h" + +#ifdef __cplusplus +# ifdef __GNUC__ +# pragma GCC diagnostic ignored "-Wlong-long" +# endif +extern "C" { +#endif + +typedef struct randombytes_implementation { + const char *(*implementation_name)(void); /* required */ + uint32_t (*random)(void); /* required */ + void (*stir)(void); /* optional */ + uint32_t (*uniform)(const uint32_t upper_bound); /* optional, a default implementation will be used if NULL */ + void (*buf)(void * const buf, const size_t size); /* required */ + int (*close)(void); /* optional */ +} randombytes_implementation; + +#define randombytes_BYTES_MAX SODIUM_MIN(SODIUM_SIZE_MAX, 0xffffffffUL) + +#define randombytes_SEEDBYTES 32U +SODIUM_EXPORT +size_t randombytes_seedbytes(void); + +SODIUM_EXPORT +void randombytes_buf(void * const buf, const size_t size) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +void randombytes_buf_deterministic(void * const buf, const size_t size, + const unsigned char seed[randombytes_SEEDBYTES]) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +uint32_t randombytes_random(void); + +SODIUM_EXPORT +uint32_t randombytes_uniform(const uint32_t upper_bound); + +SODIUM_EXPORT +void randombytes_stir(void); + +SODIUM_EXPORT +int randombytes_close(void); + +SODIUM_EXPORT +int randombytes_set_implementation(const randombytes_implementation *impl) + __attribute__ ((nonnull)); + +SODIUM_EXPORT +const char *randombytes_implementation_name(void); + +/* -- NaCl compatibility interface -- */ + +SODIUM_EXPORT +void randombytes(unsigned char * const buf, const unsigned long long buf_len) + __attribute__ ((nonnull)); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_internal_random.h b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_internal_random.h new file mode 100644 index 0000000000..2b2b7d6edc --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_internal_random.h @@ -0,0 +1,22 @@ + +#ifndef randombytes_internal_random_H +#define randombytes_internal_random_H + +#include "export.h" +#include "randombytes.h" + +#ifdef __cplusplus +extern "C" { +#endif + +SODIUM_EXPORT +extern struct randombytes_implementation randombytes_internal_implementation; + +/* Backwards compatibility with libsodium < 1.0.18 */ +#define randombytes_salsa20_implementation randombytes_internal_implementation + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_sysrandom.h b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_sysrandom.h new file mode 100644 index 0000000000..9e27b674c7 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_sysrandom.h @@ -0,0 +1,19 @@ + +#ifndef randombytes_sysrandom_H +#define randombytes_sysrandom_H + +#include "export.h" +#include "randombytes.h" + +#ifdef __cplusplus +extern "C" { +#endif + +SODIUM_EXPORT +extern struct randombytes_implementation randombytes_sysrandom_implementation; + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/runtime.h b/quartz/src/nativeInterop/libsodium/include/sodium/runtime.h new file mode 100644 index 0000000000..c1cec853eb --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/runtime.h @@ -0,0 +1,55 @@ + +#ifndef sodium_runtime_H +#define sodium_runtime_H + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_neon(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_armcrypto(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_sse2(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_sse3(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_ssse3(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_sse41(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_avx(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_avx2(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_avx512f(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_pclmul(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_aesni(void); + +SODIUM_EXPORT_WEAK +int sodium_runtime_has_rdrand(void); + +/* ------------------------------------------------------------------------- */ + +int _sodium_runtime_get_cpu_features(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/utils.h b/quartz/src/nativeInterop/libsodium/include/sodium/utils.h new file mode 100644 index 0000000000..49e3abfe67 --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/utils.h @@ -0,0 +1,177 @@ + +#ifndef sodium_utils_H +#define sodium_utils_H + +#include + +#include "export.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef SODIUM_C99 +# if defined(__cplusplus) || !defined(__STDC_VERSION__) || __STDC_VERSION__ < 199901L +# define SODIUM_C99(X) +# else +# define SODIUM_C99(X) X +# endif +#endif + +SODIUM_EXPORT +void sodium_memzero(void *const pnt, const size_t len); + +SODIUM_EXPORT +void sodium_stackzero(const size_t len); + +/* + * WARNING: sodium_memcmp() must be used to verify if two secret keys + * are equal, in constant time. + * It returns 0 if the keys are equal, and -1 if they differ. + * This function is not designed for lexicographical comparisons. + */ +SODIUM_EXPORT +int sodium_memcmp(const void *const b1_, const void *const b2_, size_t len) + __attribute__((warn_unused_result)); + +/* + * sodium_compare() returns -1 if b1_ < b2_, 1 if b1_ > b2_ and 0 if b1_ == b2_ + * It is suitable for lexicographical comparisons, or to compare nonces + * and counters stored in little-endian format. + * However, it is slower than sodium_memcmp(). + */ +SODIUM_EXPORT +int sodium_compare(const unsigned char *b1_, const unsigned char *b2_, size_t len) + __attribute__((warn_unused_result)); + +SODIUM_EXPORT +int sodium_is_zero(const unsigned char *n, const size_t nlen); + +SODIUM_EXPORT +void sodium_increment(unsigned char *n, const size_t nlen); + +SODIUM_EXPORT +void sodium_add(unsigned char *a, const unsigned char *b, const size_t len); + +SODIUM_EXPORT +void sodium_sub(unsigned char *a, const unsigned char *b, const size_t len); + +SODIUM_EXPORT +char *sodium_bin2hex(char *const hex, const size_t hex_maxlen, const unsigned char *const bin, + const size_t bin_len) __attribute__((nonnull(1))); + +SODIUM_EXPORT +int sodium_hex2bin(unsigned char *const bin, const size_t bin_maxlen, const char *const hex, + const size_t hex_len, const char *const ignore, size_t *const bin_len, + const char **const hex_end) __attribute__((nonnull(1))); + +#define sodium_base64_VARIANT_ORIGINAL 1 +#define sodium_base64_VARIANT_ORIGINAL_NO_PADDING 3 +#define sodium_base64_VARIANT_URLSAFE 5 +#define sodium_base64_VARIANT_URLSAFE_NO_PADDING 7 + +/* + * Computes the required length to encode BIN_LEN bytes as a base64 string + * using the given variant. The computed length includes a trailing \0. + */ +#define sodium_base64_ENCODED_LEN(BIN_LEN, VARIANT) \ + (((BIN_LEN) / 3U) * 4U + \ + ((((BIN_LEN) - ((BIN_LEN) / 3U) * 3U) | (((BIN_LEN) - ((BIN_LEN) / 3U) * 3U) >> 1)) & 1U) * \ + (4U - (~((((VARIANT) & 2U) >> 1) - 1U) & (3U - ((BIN_LEN) - ((BIN_LEN) / 3U) * 3U)))) + \ + 1U) + +SODIUM_EXPORT +size_t sodium_base64_encoded_len(const size_t bin_len, const int variant); + +SODIUM_EXPORT +char *sodium_bin2base64(char *const b64, const size_t b64_maxlen, const unsigned char *const bin, + const size_t bin_len, const int variant) __attribute__((nonnull(1))); + +SODIUM_EXPORT +int sodium_base642bin(unsigned char *const bin, const size_t bin_maxlen, const char *const b64, + const size_t b64_len, const char *const ignore, size_t *const bin_len, + const char **const b64_end, const int variant) __attribute__((nonnull(1))); + +SODIUM_EXPORT +int sodium_ip2bin(unsigned char bin[16], const char *ip, size_t ip_len) + __attribute__((warn_unused_result)) __attribute__((nonnull)); + +SODIUM_EXPORT +char *sodium_bin2ip(char *ip, size_t ip_maxlen, const unsigned char bin[16]) + __attribute__((nonnull)); + +SODIUM_EXPORT +int sodium_mlock(void *const addr, const size_t len) __attribute__((nonnull)); + +SODIUM_EXPORT +int sodium_munlock(void *const addr, const size_t len) __attribute__((nonnull)); + +/* WARNING: sodium_malloc() and sodium_allocarray() are not general-purpose + * allocation functions. + * + * They return a pointer to a region filled with 0xd0 bytes, immediately + * followed by a guard page. As a result, accessing a single byte after the + * requested allocation size will intentionally trigger a segmentation fault. + * + * A canary and an additional guard page placed before the beginning of the + * region may also kill the process if a buffer underflow is detected. + * + * The memory layout is: + * [unprotected region size (read only)][guard page (no access)][unprotected pages + * (read/write)][guard page (no access)] + * + * The layout of the unprotected pages is: + * [optional padding][16-bytes canary][user region] + * + * Important limitations: + * - These functions are significantly slower than standard allocation functions. + * - Each allocation requires 3 or 4 additional pages. + * - The returned address will not be aligned if the allocation size is not + * a multiple of the required alignment. For this reason, these functions + * are designed to store data such as secret keys and messages. + * + * sodium_malloc() can be used to allocate any libsodium data structure. + * + * The crypto_generichash_state structure is packed and its length is + * either 357 or 361 bytes. When using sodium_malloc() to allocate a + * crypto_generichash_state structure, padding must be added to ensure + * proper alignment. Use crypto_generichash_statebytes() rather than sizeof(): + * + * state = sodium_malloc(crypto_generichash_statebytes()); + */ + +SODIUM_EXPORT +void *sodium_malloc(const size_t size) __attribute__((malloc)); + +SODIUM_EXPORT +void *sodium_allocarray(size_t count, size_t size) __attribute__((malloc)); + +SODIUM_EXPORT +void sodium_free(void *ptr); + +SODIUM_EXPORT +int sodium_mprotect_noaccess(void *ptr) __attribute__((nonnull)); + +SODIUM_EXPORT +int sodium_mprotect_readonly(void *ptr) __attribute__((nonnull)); + +SODIUM_EXPORT +int sodium_mprotect_readwrite(void *ptr) __attribute__((nonnull)); + +SODIUM_EXPORT +int sodium_pad(size_t *padded_buflen_p, unsigned char *buf, size_t unpadded_buflen, + size_t blocksize, size_t max_buflen) __attribute__((nonnull(2))); + +SODIUM_EXPORT +int sodium_unpad(size_t *unpadded_buflen_p, const unsigned char *buf, size_t padded_buflen, + size_t blocksize) __attribute__((nonnull(2))); + +/* -------- */ + +int _sodium_alloc_init(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/version.h b/quartz/src/nativeInterop/libsodium/include/sodium/version.h new file mode 100644 index 0000000000..6c31150bfd --- /dev/null +++ b/quartz/src/nativeInterop/libsodium/include/sodium/version.h @@ -0,0 +1,33 @@ + +#ifndef sodium_version_H +#define sodium_version_H + +#include "export.h" + +#define SODIUM_VERSION_STRING "1.0.21" + +#define SODIUM_LIBRARY_VERSION_MAJOR 26 +#define SODIUM_LIBRARY_VERSION_MINOR 3 + + +#ifdef __cplusplus +extern "C" { +#endif + +SODIUM_EXPORT +const char *sodium_version_string(void); + +SODIUM_EXPORT +int sodium_library_version_major(void); + +SODIUM_EXPORT +int sodium_library_version_minor(void); + +SODIUM_EXPORT +int sodium_library_minimal(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/quartz/src/nativeInterop/libsodium/ios-simulators/lib/libsodium-simulator.a b/quartz/src/nativeInterop/libsodium/ios-simulators/lib/libsodium-simulator.a new file mode 100644 index 0000000000000000000000000000000000000000..f6b479b9eef61f86bda866250aea5c716caead1a GIT binary patch literal 1887672 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

bLH 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

8qL@@Qf|M!*(1K4Jt^gfY0PQ4u4c7;xpN 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

  • 6DT`TKbJP(! z485Ns-XriH9_t#kHR#(&`AF|G11`~4p{+KnY62z5<0ZwNP=<8^5e@si9cH6Zq#mIc zXv%WCyBeJ7mSN6YjicLjF3N6Gp?dCCwW?K7GcU#Rf){wuQUqJ{W;Hk5mLM%{O=@${ z4$|T!Hj|1KTZh=XQLsBATp~OZW_UjT3$wwEVK|o|{FiA*f`pubxlr>jiDTXh%>Bqo zt9YN`SMdR3qoVr~m3e{}z9x@G0-bT|o$Y#o(T6cEu~yTP$sVAP1UXK(<2f02^IVc$(Q*qS#+Xs5BdXLS$jsU&2Q;lGJ*6DPN z*cLTMi?NKC$-GGzX^LohTU5B2Ld&PRw%XY z_8=BsT1c?HZ+YM4X);N9i+K_D%iEbRtQ(a#S{xND?^K-R|A#h*6U|yGJ8PJQfrncB z>k`{-@!S$xvDfGO=sbb7hy2NKB59EA%mL>CnewL+O~fQ#S+URekwqO~nU&gmdE{w8 zY+)X&l@9QuXzq>@7Lcifmqb(TBTKTjJL{)NYR|Y>Rx0)Gg`q?faR2O-7!PnW7eAkrnH;J0HtkANdz8%R%R4 zENcUxpS!*G(nQpMxFoSj8B5UZ*m46n|lgp!tPev188TtFigk1*?35XeRhpOe|3NzY4RkHi z>408G?A^C@^9Td88}VE(zK`pb+b8Pm-oFPV=M%$E$y@Jc{l#h0r@A>U zL}t6e%CBR%>E~XraZ3KRZu&6X!S3-%GKcG@dwB-4FIkJdhkpN)l1u&i%5MMPx7bIy z5c`qyaM@e9_{zwPx!U;@T>FKa@@*^K>@PC=!D^4 zSMls?H>5-jwS3@JmwMbsaN$v>{r;}-J=b4mKT`Wg=*@pj0g#dS1=d& zI;T<1Si%%Ryo~!rr!>b3zfCMJc9IgeUlvBXJs zf}CovULQ-m%jW=_Vl@vwF1iI)9YXMl)qHy-dsqJ{l9=*z#J?_<7(YJrw0dkLvAjOQ z&UMZ5wG!!SN}5aD?Qz_iDad)LNbPmLXl>O=ZbweWYKLED`)BPJL~85AU?YJiH^uzy z;>_UM!@6n}nTh#vH1*a6Y^}xuPxgdm!Lfxl`RP40SK4=b~S!onnav)se)5 z%~bvoZYaImq7Xq3_1|A9u9=>nE>2t;lxKT!VtR16d~eG)vbg5be0eTX)K>Yj<=Zdc zrSg3w-(~VO%lB#d-jL6VJ0{!Zi!Bmwqi)v-ro6nmf7fvE*r6KSb1t`9`bz`LD!d-?WN% zMH0I*ku_!{AE{xbqN~_n97Ypz=Vm7-p3;&K_`}$F4?b;$Wj3Yh7xV(Corww6 zEW0upstI3t1c^Nu$$FunCGy$j&9vC^WUOYv(=q?Ph;r0C;k3wsaLt4C#vbctpPeGW zDM>cngItInshL61PLd1()(wk%@y^GWK1l)1u^MY&jm{eT2m#GV`PHEFh&5;L{y}lg zr8hx3+O%f+O~d8eF5gJ`-j?s8;slL~CtFcl<-1hA{qkKV-$(L&TE1rag7Uo~-+1|s z$#=DUFUfa}eC_h7r_iYQu9L4xK0~EdzMB=--LE}q0QE|?%VXy)PjXjyq%aj&J(Vw! zF*L4@B!+uqs^-CZ4X9XR1NIv+zRhX$6uV6E^Qvm;}8WLL@&~Xo=kf(C(yS` zJ-M`b43EQEq^bRmLSj`E!{Fk z5bXbu5I7+{2^%#)Y!XI?F2;LM;jSi9%2PsWV=s)5I%G&u6d>qSZWG*wACN^vu?m9R z(!&?15cx!=F1KP~0XXOzZMwD)5~)DWQ>s%h!``9MVbMrXs#N*TLVSHHzDWojHiSq9 z1YDYgOL@}B1eQ(}_12}`MkWZpbd!+@{IwBw!gi~q>si`uWP;!>nY8f(JVSahpvL(C`PS&_o|=KyA&QP=8UMg+Nv%7aO;O zfsJ~^|Ht0@z*kva`QC>J0v+H)t5#>UP49$`d`9L2DxHu@n_vRb0c84WwXKvGMJr7> zDMD*z3@26SG>!CS2I+0*%G`8bnRhxPGkCA`9XqD?1kNMjf53x^h=zXxMtA}wK#&~X z?{DqrNlrpg=%0J%zOUz#Jb(7H|E#_C+H0@9*4ld;&4VCf2)Mc1tkqOuYxoS(%|ubu z2Y(`N+ARZ%f+(bJplJk}%I!-5NgafmrA2;@FnfsGwHj?|t=5aUi>)%<#7$Z?2PL8{ zbZzoU!Ay@UjrF^t71XkgDcg( zKUDR{1gN>-F+2s|11yP2K+-z{Bv}JQ(_~kL);I5 zpA=nrZihG?dRzZ6p{SA5ZQS40S8=?PL-~qg*+1bz)1-@`2A=!6sG?nDdXTSssq9(x z8Z%2t)9pB3yY=N3Vo14P>8SyZT34o&v{@&%D6%68IPu)qtScX*6jaq|Q{~Gh5^%iD zD%m(*Coxc1r)9s2ZB`89cpb*^4)g9GvJwT98J%1lH9>~~MRk3uU3tzp-WKc1dx)$G zQsKELt@8k;p~(*_26=g8F8hGt#+Q9B;CQbDAE=i>?{IWdI8rR1N>!$fB4o=m1AyZ# zCk-Gz`yGOLlX1KjLlaQCDyr0g<3TBNcNm(~kXVz_Y2~-hVi?C`01D8AEdaxtYDEm! zEaX zA8M9 zc&818y~Vee@2z*y0_l!{r9T!f41TyACsWnOa@@yqoCF=ZU=xJ-u^b2Q3MVQ5Ka}I( zv+<&93ZMjdP<+ZY(|N)LD}Mhqr99z55WoAHN?a!j?m(Jb^yQOGd~Mq57Y!znKV>ip z{QCzJ*}r>mC4A{)iS1*F%|z{wB{nxl{jEuCjNQon64~J0eS}(xtlctgI2o}ihXmR{ z=g|Z3KDVrd50yp!k%2Ot;nWL+vHNpRsO-DK0{^2=Me zK6H5-SzbUX=cB*#rN8r~zw=Fh_eCDQqo6qodUyx`P~0?|rPgQ@4;B=wsC# zz2allopg@jM3$+Y^uWJs)m;{dXI>h=6E0<{yRX5iOm+8VRCi6Ly1NCR6x{hPFBz~+ zn!95B%W64luORa9#He31xxj9d>^5ccbiGZPWVib#m+I~QNp`z?a;4txo>U1N{8&2t zSUUVzI{aPe^Zvgr9imp0JqQkk?0W_ti9#PmANDznC9iYwwQQ+!!+0^$W<^3FH`nPG zvCt98z9S%lr?-|Wf&9veI*A$X0Ly#5o>@Vv@U?m_8X(O#20EUv9{>}Jy*#N#nmC?b zoMfsyo}Qm%+Bu${n`9b4nBB{hgLwXnlY@Bv^OG$eL;o4^!{;VjWM21uh z?-B*Y^T#$`nhN7J?PYU`W}092?~iS~W@A9rC+qyjHr|hIyfDWe1HuUC0!#RXY`ha; zf>{M7EAlucYr08`Jl>yGYVsnFcW1GQLIV9DE>L?r`sfFXH2)5?gj1M-hq}W1H3N4V zL;vo}Kb8zllku^oS0?(8^@jhe^o9(eS|RL&EM{umszW5L?dET@Lz0!|a7U3@(6RMv z3`cbciy@YaAr=E=cN@d8tRA06=a(`3AX%BM{}MHa=7o`=Icy`J9`_8>9OB3TB}sU~ z;{M@|481gmflo<%p7A|2kMSp9dnOV}b7*|eG>2Jmtp#l})*=uFzhgK<_qRSK+zf2V zCoQ5pM0@>~1nV2V=+;e~_x)Y+c{m2KSQSUcg;Za)LWId;dCjt;-1Op+{S%5x{*X@1Id%@RKWb z#%rX!&-<-?-sSb389y@9mJn1t`uI$984Nov!Y2i&-_113Ry1zK>RMak0zUIjvF(U z6$m%mGK8^X#(Z8|XpS3yOR!yOurA@TS+{88z6a($5}R}L=!YLIFCT3+8~qrt+|s=u zp}+MAtAvmoLE+XTXi4aWehEick>(&y_#UoDaO80D=ShDxBR}XTvV`?l7IcaG_mu|u z2v3P*#suTpgJp`O`2*KHJ=jD z1_5tx@D}~WBCAR5q88o?>n5|0)}ll1k+lPh+LWSp`Dukb6~8`$OL}zSCMsP#_Q=>> zts@^^a%bJb4ncx*wP3%qcHrSV&Uu1<>iCZ6Q}x81T6^@4r{0avPZ_9!{KUC)0eIa( zq|FSPvUr7Zol<+ScWA>8S``|`EjhOq@FLVX$hKthYSnT|?HTjgRwy}VxX_o>SwrC0 zu4Xwr0f+E)hGYPZOmWDeah^#KL#QcO$V+2R;|~^ZRu_T zWrMIqKi~@?ZAFXmSYJC}ZQ+3S;<4w7YFox`KXvTf`crQfUw5u>=+@!^2TSS(14~Kc zyPqhzta6B;16ad>fjgycnNdmIVgXk){zUYteED3~SrO+FwTqrnQMI~9$C|2n2#b;H{vyZ0eX4Nr{kiAZ z46ZO*{7y}S$S^YR$~)>lQTql?1At#Rq;&-Q%K@9U`{IFz0_>I4p>=L!udST1+Y3+4 z&8^*5(;(bM?uZsQ6|?Q%^+n?w=l^L@ZQH5lq5+2%Up3hEgUjYU*!qPf?m05=3q{d? zT`j~a@~yWozUoiTZD^hN@RB>8>KgEGYOCuKMWfm>5`Rt!gr3&V6fVB!T@wqmn57mq=`Zf!9UIO)zvFCcy z-%)#-HTDB1MzaTA9=&(r@B{=99hGs1>QA75D!M!yacALh5eRZi=7V~h7Lh#F$o>MI z*>wN;KiA%XH9`x5*waCx*mJ|xs|i^+TpgK*XNIe<@PIpd`_6~mJ`C(az_vW5WoyTO zG=)T@U31u_LwSv{1o(}k6B}OOUgVKl+4uso*jX-wXDb|W64eAdT(HSO3+b^ArsOO4 z-0&QIh7z}O;d?Z75Y-;v*_DAeGZ^D>Ul`4c$aOO>BG)-`u>CfGlubF@W-Z8}?MSOB z^ns}({2x7z0&x6sq|mnUo>Q`iRzdq+~N)-h0LfZIS)S7}s{whf28 zyV>u8i*~_b4-IFt(W(R0>9K8JB1r zO5q|%W>iN&;s9yb+D%}HE$t(d7txklc@d#mP3Yd-6?Y-I)Z+qAmRh-x+D2;Ir3qBU zpAE@KyGylq>N}{g{Y+Awe1F-f>p19LD{Q|l9Q$vaU^6#|e8P^X1$=_K|-fmA8Kk9H9;3HK+QoF^&hc+IbzqCWW-MYGWxXsWu(}*e>oZ#N3pT1F*~~GMRlwJYyM5+W9Il8 zy>a271hE(!2L)rJyI)b1)kY6nI4I~NJOo3dJ+SA_KFu#Q*2;wS3MH(Z5jK zJpW3T6-$86SXS($q@MBF*7JSuedUpnxVHGTgLFwWyXbPZMkyNH79FKa_LMAMo1&wh z9@GNZY?3taa=P=t!7!GA+EQ~^7OzdbJw2$+YTpgwi(_G7u|c0;YS&XfAy<($Rz8^V zKQ?7>4%hn!kKqcRW9K)=q{ZTSj^VT3j_QUDw8WUT`(HG6_ux`W)DT)exRNI=L8;vE zg03E;2%!aHCGh%osessL$gy25EG!>9ow3NwbMB)JO*Ull+1C<5pRpk?LWS}oFgb0L z)&4#?0G$oVJlpPAC~8tbXM)5jhJph&Om}KPIO~RF0^c2fT>2Uf4u0|kk1aM(V??8- zfc7dGxT*q_ZB!-a3Pw*GFS#%j`;s^OofJ!r9^3vUl4z*lix#B000#S3z*azhbenPh zjYG&JKf2Zp)Mss=));6Tw`pfZ20}(WleBFB$9>jzg>(bBhT+OxptJHE7ykpjzWIW# zZF)aJVejyUyg?>gNg8Z^=(YzG!f>?fMi>>&i?ry{Oky=1<9QGQrm3R#AtXC(C&}(; z!V2giSv*%O5o3~H7+LwaO`w|)qt|%R;B0Y_O&XF9+6cXe;rMKTcLGmZV4Irc6&s{Y zMnhCcy`!8=Mw3gmNsPNy>LI>KdeRG0o0BULO&;4J+)E}BB|#T1K#id#5yZ%k9yZ)> z;2JL%X~(CCwBz?fk^n0QWJynMFh8YO^F++?hX&)ee5i(v18h)!!v^Jq2IVE>{LElU zzC1iXSkrwTo*rD|9@qtji9aD1+j+=~xpUA6r4xczW#&uCuq`WKK^t-^YLdLmC~^Y5<&LqTQ3H*9)Hg0oWYZE`FZ-5 z4RyhMQsc1(wu~XKdnDF)M(^kz>w1sCUdH(Oh2qomhcftTSPhP4pv&ra*}y@UgVwCy z8#a7IbjZ+8^nG`+B9Q}V*80=Ow@Km0?@=j8`>383a1VM$`s(N}at8+!%GkJr_p8&S z)B7Io`ra74CwbBLf9Jw)^nVH7ALS14qc?$Mh5s_R|1Edv;}-_^Il=ww!Tn_->-*1x zyY}T$xjEd4BGskcb?lq;@L_%ajQ31E{O4ReuI`t|jp*G!J!ih7HtO{rnOQNb;=#%) zyFWIIeT$^RcFOsg-+h$Lk^;Sa&*@zcKkz793eB9^6Y|LX2j)H$ndx-kE`!HrJyJ1i z&O8+IXu&D-QuXY4pC8w+p1rL6fp5*axoos{$(cLrvB&zT*(1NHp1o9t3VxxUJ$(Ol z_3TH24cC+(bP^f!S-0;FFEuJB{B@seGu9a<`krgk7UVNMNRY_st+&2ZJ$pe=PqMLJ zHgG|Ik#BMz;DQ{byXOm@8&es?ui>6UT)Ip5K0-lK%P+_?47%uhAa}O`3_^31L4R>Q ztO&Uz7R#1!uyToBqp``_nR(_IF83C2d1N}9P=B82^ILdWkj<0Z<2PE42pV7A>@ry} zDMy(UH!G4vjt1Mwmrbj+)-}GUg=k1GO?+9j$~|x4g9K}dv+?B#1tJg~g5gP3rNy?A z_@e#|V`(y8_UmQjt$G3IL0@nWZpYPTKFKdFHlawt(#X7Sn`e`CaXn9SB0!;74bqAHnkG|_f>oM^bX~u(!eE49GIRJlHr| zLcZoxm0jJa*Q|zZ>w(#AgiH-~T8aR6!!~o7%HxLZ9m4i;q61iQh}96^K&p9*YY33c z`h1}q$}#7GWkw9q@@1kCp?FDe6u+(>utz91)#+;My~WXTY05YlS&`|STKI0X&BT(a zST}8hOgJ@W(uiiu~)ajtpw7I)a z@WfJkN|&drb) zi?8ENn^-%ZrW&GdaHK#EJFO#%)2a8xg5D)2mRe?rbh~bpi}z`7{?w~d$ZQv7tTDx7W_}z26~f}HYjHRrB!iI5fHeG zORH^~E)J3(ZFM8XE^G4BPB8=DB4a!1T=g~Vg7dX%oA3ovjxWwOhM|4#!_O^211Gve zj0V}@JV93SD0bNx0%#wI9<15cO|$3(!Jz_tuoFD?3t|B)y9_md)kw}%Vssm+e+W#8 z^_@{nDX7HOAf0Vo+}I+AZW=IXC9Sc<7NR%xWa)HbD~;YrFO1M^Zsdr~*Ou(%Z!6=s z1mt__hi{tpW`~Aj+vS6%<h!dU_3>p$8GR6!c-dM75)tjK-A|@ou5J0(fN+Z*)B2#S#Cq$_;|vMG{#zsE3^U*+5EomyO0Z~*qRb-{WI2fHau8roNdJa-0-ogok$G31U;LE%lq+6eo)(4>(h%6mP*z?yRBj_HU~@yI^rWv5sof%J5@G-% zdjlU`MDiZZDn7%S7#G%USC+()A3b2#EygIFBusJAz1kTp<*I`;=&|KXjPlAgxN`&v zp$sgQ>y2@(5qg@8k2>-+QNbm!8BOI{#eh2ED{>Tr)au?0ka`o%=`f4Dh&Eoby#-_Fe8N@1V!6{az}+e!V?+(u zZn;(##g`d(W*90BQqWW4?j=BZcs5VD_-Z*m3e4f0yinYxs+yF(vc@HkF=uk~JU4im z7{cirq?GL8Qb>?ShA_mGLpE_Bioj7LeE0PB= zBS0CFR2l8vxo2h$(1b;X0>phNHW=vN5Xt68({7~ifQ)0yoOFe02jBzNBkPp;L5Kxv zM=fzV;9AF9Ws7k|ZGuFpwAi>-G$SMkdbksmp(Ez$VBoZUfe>RFnH5L#LXxd)QT50K z^F(a9k(wqseCeb(>nxg_y5)hvAQlWGG2&vNr)oQWz4YnVcZs z90KXObV>-r4xF)Cch{eWI-)F^h$z-a6G<{sN48x=W6SDAzkq7iHVx1_sH!nN1_4=p zD6CmHc*Bqbmyh~yfdtLcQV8~DHDN0;jWPk#ldeFMTM$NKqZTl!Q)Arp7EoK2z;p=+ zf>hbFD@=}OWDc;i+&UKNP3(CG6kvw~jD13ZNlD8Jf<|TVs@!8Av46VVE9_JoF)b>* z-YU$ni?Bd`>OHEzQENyghXAKr23Mbqxufzxot3{6q~QNT`N&%4vXbf`C?&eUCC1nt zR_zPe#{Yr0~k$`~w=rO~?X1i)&%#S9yB3lG)Wi;(3 z>tqIQQ1?)@I*e?;C=&?Y$s0Y&5r`X;1M%Uc>MpU_k%4%6z|PfH@EU!ALHpY%bhR;P z2;>CCteh^CqaLH$8&&t+sypo{ucOp)0`Sw)oKqOofDlkhSY>gqL!iW%hRDDJC9Q@M zn`WxO-eV^LFrxhEaYIA1T^(W|6HwwYFIJrEm#2{26*XNWK!3- z=(sY@J3hWUVTb5t4GcMJDYT&3Os4+vAB9|_8d zzU>694=9HC%48$;r=JC-i{YG)9|R1(ONFHYy3aJ+1>{HD3^O8={OCrzqMkGA4aF*O zqWu7M!en5>mSirm5WZ<0C|bEyA>wJ7l%f&K(SykvZs526)6=t>fCJ%@GEi-#8Ui`= zJt}NoV~`0I7E5Jjm+Bx zvRR52safT;5p##w0UD?_R7ex+XaMFR9k>n2l}uiKU8ZYywMndDiC8QBMGMn)?4HPizH6#~P11~=!eK*p z`s-3fcru|0#Nj!E)@jbC4T&U18?O6^ef>^p1CpqyN6-ufSolTIq18`*`@})!T|&O8 zF;TuSOK@~yuDs$E!n>mj@NSc^iy?{(xkHTNaL7o+C{Wk-^-;->zGCb`j3U(_OOnc6 z9@vT)+)U=EWacTRe*sc(T=6If%Mh?U#m-<5J)Cr#iZb($ipsV*gmH(^g-6p(K4}Ju zStzsi2wr?pdHoL;uJ+>3%rg73(XjT1tj4%8!zNYl{HSEFR6|6N2&6G?pjYh?tujXj zM$kYM+bE3eRCFg(S?zo3s5wHs|?ARq5Q z*6t-Mq#A01MprX0GpUk?ZPxl+w3QvMg5v+8{r5x`CPwWh%*l&JAchPT$1e8$NRG!Y zK}7HyWQbN)oj93(N%cVR@RuzKLNO`F@69G;`6{#8`fn33lM&5dWJY6pBLS1gj7{JG zBm1^3GmU?F#Gh|_^4*nBe{;}(`Aqp2|M;JGKJ}SzH57hu-R73BSABNDCx@(m`NSjd z4xRml|9iv2MgQ{1zyI;+XVP;|%(?B@=Wm#q-*MN93;D?eehBB8;R|uJrR=N!2nPq< zZz6{7@_*HRTX1g*?*AU#xyi17xiZZ3N73p_^U#%HFc z!EZjm-_2uYJ`^i2<5aJU@dHtXA~}M>t+&(}mlyg?mjg1t9HrqGzK4Dw^5xL)V$XjX z8g=oNpVC%d8Hrr4piA@v$qw?VQCfaua1aV%(fj#+ARO%VfWElj%$Y@ncNd0ltosb% z3W8RVpZq|k;}?~gZuZ?Z@pZrLr| zGyO`GkN(wsa+K;1PJ%!5r-NJPhKo3L$Y)7O?TP4(gW5qL$92|6vj=cn70oWpw$nS; z6*nutk~&sy7Qb;vZF=mUy2YqcwjVn6%RZe^o$iLhXgaE=$~Dmtzx% zu39gm^1s_p`8vO_pYnSZpQ@WUq_+62-vo#yI=f-f_IWb}LQwrPQ)<_hH2OJUVT6NX zRiQ&eg1h0Nxu~|drM7y1N!?J6u{u<;c*ZIY_~bCDo;r6Q_^CS2=&7$O@9UZ6QP66R zX%wFOEvTe!diZX0R`+^5mBX=PHPA>&=6c-ITEatM^vU8OGMUa9%*fQN zhsgtZdc&TOY${vqX&6rlo!y|%hPFlx}IUmqRp9{LtvW) z<|?qs9&gm6EA}Qmx^naM=(4&^kAeZO5>xBq%;N4V3fw(7Q9RyY`WC6)6DES;+oU@I zi(7&RrV>aB2`9R!C`V>oq5jdAKAxC!y*UF*Y_K-3|X7UXk1f{fX`f-p9C< zwQ-QMTq4le^VZ2__A#792Xd0`n17>06%ZVDEqi9D^k96H*2MO2iZ|2*pgFTR-@Kye zjdpB%QFK(rk|o59-&4<)4`y;XBTLh{7o>eQdyaJdYyzeOeN4N_@doT)v4fVFdvX|$ zt!Tw8C89e;PFMi@Ab||9z^(bEPXAVbTvB|=BXJ90voIpaMLtI9y>q`#xepF=m(IpN zA-IkjTpcfvlKmD*qs?_qf$JdZFo@7`vI!#}bf~!lBrkGGrFZm~P=(n+7N~x84hGT< zB2D52#F>P2Q?>z`8L&)s>q_EOZR(B?>JH9$KPX8el{EC!Q*LGbrB-pLtZtfMajJyb zVn~vd8Q_rB@QBsW-1|}9F>B%Y3}O5jorD9bEEx^yM0@AzBl8_3%2)yG~tJ=9BKIx9?kmb$^nnj)2Y#e3Gu)fpIh5uwVLv5F{}sRLxX z)KZT5?Gj2~vD%@RCxOkj=wjz#(c#|V(3K*M5JY}nq6{2@dUS&^n$}vZ7oA9v@2lVT z(?={F^e+FXyvXSLj3een2Hm9xlBr+VX+xPAsNA3%a4_>zr>~4Yd6x;e>3~zE9jBs1 zb-pt6d$TbUq#fTP!xhtwf!UhVGA%isEnf<3_SnPZ^iV;H7+S`1Ecq2F)IgsTEc+3b z|C79a3iO5@HC`q^&wr?4oTWYY5%BkVChYGLF6>Rh{?dA%4sPGj%7P}@8lUJT=_;IqfXLTmqQ*E*wbTY1)Su4R?@`F zvU$Uw5uF`xcTVT+M@t-3XmH~{)U(1QR1gUxQT&JQIj|~#C$Y|D0SmV?99FbBrl~z# zV<9U~%e4m1S{N-`4;p973hSJnWO6KfRbl3RfypgK9pv;?VNOw+24R;hj#e1I6Q&=| z2~b(aL(E!?Q0f2toq+hYQo&sBU{=L`sAq+>D2#Toae;YQ{D&5T3D+1sXvG2_Ted_M z;6+S5P{s2mGK<+jL>zQG1j%0GMjMp0vq3pbTa}5~Y&6S4z5=8G)um>Mq)$B z_g-+)uoirmXeEux)TPE8{^lgnBK0^)>vsa%;ZHxRGG=UQ}z_>QW8(@U&P*K<%+!0 z@_o{Qf&!U>1y=y~b%NUp-X5}%Ax?G>;fu#xzf99zVxJ}}Eg^UPYcCI9$x)U(%o(F^ zhiR?f=U?3}{$ba_CrYMaEp?sUg5vr1t&Poqg*WHVRHjy(y%Ut&DPK_GKq7t%N+NrxBFSSgJvXCxBfZKlMs zc=t*iBvVsSW|#~(aC+)#sU0fLPOGD(3|bXxZRKDNibb~A8@AOjk5Xf zi!OYYPNglVn@Vce-SJe*W%EAGgY^e?RhcqjRng7EzF72wVfiKJ)^S$J{P#*mHId=j z@7}f~`ir6ot8O0n#UBjJ*W(K}7kzQS4~p^!=;gohlEq7wUj92Tm+^8Ldu0qP8Q(eI z6D&4evq?QyDTd~r%$@xM*E zWzvv`?~n+M91XiGhats3p$gCYGomF*Zds5`ye8sZdtSUdv!6PhGw%O4umVsX^svVf`s(KoHDXp6B@J~o* z^`#>E&3e4@fjJN5+;a2ivZ{Hra=vw`?=F|_wIil#QJ8cI`d!9auEDJ7`aRZiMOoAJ z2_8o9kj00~`QT!G^z|=@^1y)0XpP>#n&36gy}$3`+WO7re?VlwdFd;IkcThuy(RcQ ziV3Slv0Kk&5PA=E5u>kX=8TSL!qwc77DtZ`)|HJ6?hl~Ufa_fS)!=?Zpm)0}xQ`l5*1U&j&U$1I8@kvysoT5pd0zWQK9ee>}k{tvk4@SdqJ<<)-5FUT_px%gY4Pb(!o zGSZ(gPmg^iTi0olnHG%E;~#0bUonP}MQ>=zcwLmESHxXKyLSwi$pu{QnvO_=o0{C0 z;uUR39pw@u;`c~;SP{$_FvK!1lr|+kbqV9CvKZVC{)7E2VlWQqZRkN~vH5C>F(OenE~`KN;jGFDXrH@kJvibAFVfqGFq} zsSmPI5?n8F0=I0Ml+89XYw3d~%LWiga9d3=m2JiRguzSSP4PhyF=iC*JCnZFoh~3h zx^;*QdTynXTmR+RI+_AC*BrHoO}pADg|&1b)uwCYt68~L+Q_>S+0mQ|<+K=EP)b{V zL$fRCO)^$t5MLr7&ilY-i460H6sFnh)ldAs!>G+f*9aqAuut=yq8+B~8*{$A2smN_ zK`GL&RbZ0uUdaPkBxwO-Zr$Q0VQi10YQjn5Re%c?=>+(-0_2jt+3>5QG|i>zEyJHy z^3iJjxKy!l|v|S7I=b z7h&$eY5=~2NA03UxS^aLL3Mc(;oumq9^>Xa`rZ&MN#tDt6xH$ETa8@?;ncv5UIW))j56cv1fTERTj4~sbqa_ z(5~GmbepXhg}x!~A?=wK&{9b;feJ%>pat(C(Y|<;ZfE4qPi5e-8(&gZP$$DGnHqUQ z(T+S1lCA<4#u6deSayI*<4GdXeLc8@X~l%g3hQW6!Yg0Hq{tJmKd|0So^D~Cb#IX; z9u*Fg2{5@9O!gs9Sb+Nui{fZ|OOPLZ)3BFGp7=$ABI(5Qqeu1ZR!X*lU$+dvoI{() zz#Q{q=8Nu7SQAd&8r7db?@ zY3pYh)kB-*c)a8JVCo^T8qjI}h}SK+8A@8`h|UfjR=1ZkGILklvIFXqjU7*k52Hhe zXW+HoFYeBm@ys$@XZlnAr-sjV< zrfscwpAb1f;L{AbGxpf-h~`r&tV`8x9=M>)SfkP118jLq5qS|Y;;^IAhqO?|F-N&s z^`SiKuH_NN4rBAB28V9|IV;!Hy7+lgL-hZ~DqDpgy_kAnhNxP8qF(@2`z!^x~&8;k3w_sXUwKt z^scc%7cNECVRx3;=4 zIqkop4j1~II?l!bq9!v^7njZ-@tSB#ln#>3Roh%?7$>QiC^LUVMYHvWE~1-TF02fX zC@#()jI^qWmeEpGd z%m>-|+r{Rqd}(B33|Cr|$dddTqBHWO&JxocnAvGl11)R?i^Y zA&4M;p(SdFBmpnmPXq;!B6LAx5k#>3LAUkli&U(lVhKeX_H9wP5t^-SMF(jMDcv5W zo24`Zl_rtc{#SE3}N#6gq_)gACMtbJb+6wWo=!>`}Wl);@oHGYZaD$r@ur&)e6f1SU ziy%iuX^{yCx3KI48^+az8tt6rxOe`D^+qpS$OyX+7r6~;d6U3CYGA7ukefJqz_2V> z;nxOVT~b7p74+rAasjubQZ_B6W{}Nf9AL-8P;r*nG0_e zrdqi55}~sj3y=swb^|-+pQdGBSR-72Fuzl`sudz}yvi2CQMzPX^M`hn)WfS5qaLCh zai|_rq7_dQ=%g~ECF`4qsISRx$b2z0skU6ryh1DJ16qt=ac&S$i`#>~h^H9w* z!x0eDJ%8BG=#UJ5#|0ZWkxwan;3TEBnnZI_IAVE@0C~+&<#<)A$p!*7HnpCw<(kIX zyl866G@1+QVRuBwgG@Z2PV^4joWxc{8&ydfFb)~WPZ|gEB9Ndjie!eQSFK%R{~j6f zwf73JQfS30zA;PSkzOZmloah~fCziJ#_=IG}Nhh5v5RJH+oC z@B_*LP};&d;+kY-yort!8*~g+v?C_cODG7WCBv7qi3%;8;doVpwiMs3nXDCsd5eNR0tvOn6e1 z47n6|WQ^~-=e7{X#72m76LRQ^XHCdc4K{5dX9>0@63}l_Akw5wTUT$A9fNkz@ob5y z)FP6?V&gpY)k1^x5IvHVNzvL6Cs<`_Bb`Xq^c5^M1+k**@>xri6SKsW^OfM`09bH3 zBNNC?93jXks(YMs3rI~GXNU-e%1~(Pp{R?KnZSl)=}6r ztLqB3$%8HXK`?PU6xh-)Fp_JrZPvNs!Gd+j^vi&8g&s510fbhA*QK%w%}M+s{Y$ZS0cgGDi_ zJK}&4CgPayX zhib1iLe~AQ?`E{U4KIcfnMwj-?!s-q!6I$4hNbq#ZiMJISCnK8wi-v{rnWyIM0fIr zA}pkQ&A&tW<3erU)zkI}9?S@Xw%7D~iwg+b-t8vtM2~~k_NJ;Dt(#9sGG=Bv7YiJ6)1pAP&zIs!kB1_VciFU4^}_itgu);%xq z_c(zOS?~Xh@mKfhTy)O|65Z37YIQ#p+}{cAn}a(y*>*2(JrvjI! z1K)Z4=Khbr)0ZB1fPVb}RG~3V*LSb%E-^sqD-b~ z`|$^OFgEA0dHQ0%zWeeP1o;#M-*q0|hdDESlz%}@2v?9_J^6)oQ_!XQ7u5HGFF}6t zFZc=d4Nw`}T!9zzFZf5|L#mM>7$?5TJ=4EHohAHNW|{EvdQm_57Zl!G_?5zY3h!n; z(*71L-l5Er+PiaeGJNuC7v`2KSf}cJjlR|QRK0n*oHF)AZo!n=qTIasH@Wllp2*F~ zJWa1Cw?t>@T^pRGSCo6x;d0=2C+YFg&e3~0w-lx3c?apeocj}ccJGt)C{yR?0U4i4 z>L^$*-{b{{L!jrSk{5T3-oo6+-E+3WYm}n)+1wu}totOrZJ^AZq&F)1nT3Ww&eLlb zKqYlka!dN2ruQ?Vgs17f795=i{IHtLQ}Rj{6Gq!)hD}-goYnKZ1NENBE$wrh-m|$& z1gtV4m8y8KmeS1ir(GO1^63dC+JedpD=&CCjS6cEbsJSnHRT*m-#_ z2k-QE4+(JloZ|D8x-g^u&dW=}k}h;!-V>l;-c{$FmRIB;DY?^5%M<;ae^%ZTxu5Ai zDeph&@Vg0_C*?umm(BZDYa!0#j$UdAiOd_%;dk?L@5?+ZZ<6ybKmWWu%i=~$aChq; z+F5zO-|MWrUjEMiv(L-Rp#jc6FKJkLLpTysyP)tld* za9_GojeJRXadb(^!UbHtb9lsfX^#K5#^fvoPJP+-G)pbX!pIqA&IXynPuPd^B zny&ko>UzXNPFl#WrCxCexChWwE!kL&8w>RYZ(V@9yL}B9O$4}W2HJJWHPw+1fU?m( z>;S^~sV1dsRJx5yw-exAvCo@Rstm2-Z+19uX!i}khLoT`0vSB-=;0ju7gISdJqX)j z5cF+5-`m_%&7>ZneysqywI64UE?MSj{yNw0}%ye>47(d$}8rZ+%ATEn6NEMrQL)QAvzZ_&e%_BWu2T{MQs9N>NEAGwz_&kZYuA%g#1;2*MX z(c6Lc4y)*nR4X_?>h18iC)ZR&UZqtX%68Lu9Y#77MHeJ}r_%GLY>V3)9j$hxwn7|G zw!cZOxDQC0j7GasA`j7MquLEDcY)vqXiaGr&nRFujl9DsV7s>iD?QLd^sPq#;x&8T zoN5Qk?Ib_0JkE`Wl26fE>G9r#_Schzqxm)=Wj$|=b)l75GOW9=Y~7!H@E*2MPwtz+ zZkx${vp6;|xppebus&7XLd}keJVB30pdsk+4X*Db*G!FkkX$o^nt71i%Jwr*(m8LX zAWgkavNKA1GB(?UekEk7QLAE+?-N#K$w)hIjr37)^`E(xHX{hiPURGbVd^A(7b{xI?EA%2bsM$ z9pNn-ZnM|nmk%y=bQKOjj^}Ft z+Me875ospzi-YUIuD7}UthMB3hPmCTgWhic<-slb(8Z!7uR6AHa6)er+P5YP$5BAg zW7KC)CihN_w0RZn>)of0)CO-gQQ|10HAbxuNXeM*YAF1k_Khy4z*%jCx!qeG_T6!< z8-PYb>2@G_mw)x$y<)J8an^y;>czYLgAlNTdIE#88!%5ol&^isQFZIdvh%eF;p8fYF~*lY#)l=P7^ps!&ZL$Y9I?g>6kJP=fQD=Iw!1wY(6Bf@ z?8@Jj+&e4s1{iboqzFBBfX)SYgV6|EthHBayc56-1Di2M83#VlityL|jy-!RG0#lvJoJTcq>AIWJT+dcBk+}bs9XaDjBH=)hT;`K*1&%?yT^jwlP{=)Bbj!O)LS%C zC-+W>G$qSMMw+~FNH;rzj$qO+byu_3(CpOFIzQ;?@Hfzbw=tl!!(3o8UP^5OEmC@> zSP7t#H_ti7ka~yAIK@xoVB^1a5{aj2wzZ=FyB42-m?g>|hxphy4$mK}442l%MqK%@M2MQ}#}Cil%}2Q;gZC=I)8aU<;DXD#bM&?)p2G9Fl_7Uf${iff; z0u$(5`a5q;;rf+JNz~6WlN-@PfzHvt#~o}=$VY+1{p#(c)BDTZ^?h0J{*S!q`+w!m zZ**<&K9f608NHP@Q@GZ@>HbshgSamW?sJ0s*Mj>mh@|g-AKY`et6bfQVo>WYb(z+e zh4uL}-ZS^N`kaf;)&26g5xx7T=gfDlZ_9lDU7xomwX1-+RK4!4xAr=3E!69tx6G@L zTK6~B>oz0%>6hc$ITF7Ax_aGPGxCE@B5jC-wDYCwb%T6Lm6o3*N9gB*zmL-E)(82K zjr|fl2)^(ad=6jPS4{h-J@DYXne*p-_t7~I&73)D-aayGeN+QbZVRuBYZ4GTh$Kx&b>)^980YN5)KhWuS1zafBpX;@4Ed3E^)%d$mTJ~$ko8s)ah z#V!#us^A?V8k(3d*Uy(=pACLgTGYT9Il<&duxZRRkd=%Ip7l#5c~V|iWaBi%Jx@Tb z2dHYiHgdWtqt(DIuj^M9A|-(0C7kQCxtGfAa*aLc|)PV zK@}}Q-Xc=cs{{de0fvGVt5!<%vuL#@fjr8Z^I7tJCm+Qfb4 z#Bto)!WQeVD=2#74NNLT1SA^S`WB^B9PqsIHuO?i?XLMyj-_nsi>7b|Pk_{m)7<`O z)|O$(2d~cBW6cM1Qv7{V;TjQ-L=9Iq4h6PY)rV311n9SMq`wE&5~$3^wNkxrt7p;Y znVIanLA54prjw`^q%&@t2B{{!u1eSkC~K3(tle8gM=l&xzBhUMjI8|-IWu~p7LDJ? z)Z0cX29iE76^XOP!Xe7*nKdtGqS751G;IS-n$8yrsKUbXcax137{Mimq7Good`0ZO${GJmFPW927w%q81Z))-I(#yaE*H_yZIj0Y$q5`p$r61<-Uy zZ5){i&Bo+|SFlRY8(tBTD=-{cN?Y{<&F&XGvP#}QUz5HEM`pp7qZSirs6lrTzX>!0 zvfhLqMrJ*mTLZgp6G~A1eJD89QiU2gIX&R~c93)X@T@lzF7E=PosU3GVO7Q3URCA6 zY4Gv9w+8%{kyALJ;_c*vgR%~WtnQ+ zhh@A|g+oA$8*fiG&M)7Z+&4e#>0F6amIB!{$U1~Ua=75Y(qIyE4;)hV>`tLF^A8|9 z2SdZ|)&mX&x=K};TIeKtljhYNCe81FvkI)GDBP68{^VZSO%ywj*^`ucNM)KWFO=6w z7p6GBOjG*?WwuBv?7vL3Q+>-!gt0HVOj9xkW!8tebX`n6Yr@!rwnex#ReOlr-QBp|0tIfB^v-Mw*ME!{U=OW6cLze=Uy7rux3nNTNkv3_^| zivO5?%1`=_W;Uy^EpRSzAD&e~J~hgWUk=KPkJ6`ns2?Bxo%)s975xhNg>?&pdfjHu z{P2C%>x2Aqf>hbmQ+lXX?0b?4XPD_n>3b4Ee)U0uF+qOK-&M3-?v`PTx3aDGlbG9#vfLoy{KLoxP%X zRY|W+&Aazh4=t*fxUG6%2Uhvt>971hJg@v-#rN4${nrHI;U&So>N5mFpMBMZg_Za& z3Ja@}jZZi@I4c*3+;&JfB;m zhpt$Pf^W)|71OHE!xjgMKA*i2MW?_$JL;j_0+N9v2U$^W4$(rPDJE6g6Da8lt7DB1 zFTsx`{hGJZSEO^A?k`}&Yi|+ev97qn7Nel@*znrxn%g$a>uUAA18M545;yzn!X!;E zc&pRMb(9h@l=*ADj*L|9Q;SlqNaH&~aiuP8S~Bd6^z*q1eO%_R$w*h-x|2E%q{eAC z!$hdwNHTP{m;_(&D$?_)>0-YL;I>^w!ehg{`K{=`(whNgST3`d9gx7+2K{H0*U2@5 zFkDWYA35y}@=qHQ4W1ooTxG`TQ!!S_)yfQwYmJ~;k(Py>GUS4jkTXsTGRuC#o^u#j zo<;~VjVJC)M3^Pd2xRF&=_x4d8plza18kuy++ogZ#WTuq+K~>?1ilg~sS9OsX3SmQ z)#>-qXM@I--5HE-eZyOv-s>&*lR{E=wvFDb^i{dE;Rc7B1st$M+Vm!h0|}C$j%48H zIPD+ku~(!|d5f@#Vj)$-f1rj}s;6BmuE1f=oBQfjfOcY-cc68vSDn5ejIZ#6=CE}5 zuc`H4C7qkGAbj_xnRR?}k{)O`YE0iv`ycTGLc-lZE$YAE-P1z|G|}AGucG`@!@S~F zY}&)qc!}Mhh8j&fET;`SXrJoXF>6_ha8p9pOw}|~bqxPpx&Zp2^e#0HEi@9Veiwca ztJB*JUbS-)S4loq(EMHKq`b5!uBT;!`9mhgcY?x)=p4&?8nVbJG104Gwt1VWPm3CO z5e=M~gF1v2^ALzw2GXlz>ma^HZ+CaYR=2i?o9W)m{^?NlLdV5$+;LoNcRHZM>M~kw zc9Kle5G{e~gVnKT9cx>WelfR3K#Z(7<&CTG(J`L&x+;{6`5Fu^**HAC zM||NJojl-zGs-rS2H(E)GmaZpQ@U}(<>}m9uc8cxo7pL79Ti;RxQY$ltn%&N0Zm}1 zqEq2IY5&;d+N<-9q5Hgg(&4eFr1Lfm!1G|znX$o}RgL2@-%seCj$g_D*9y4&H%qu6{zFTcj{m(SToC^o zOPG%T(h{cQPg=qqAXFbz<5rLW^@?GxOs_ZGCG@g_YFlPFmpJx%z`8(J*&Z<72p^|& z&Bk-Fe5bd&0uP4ry`U85!x+mF3~9!M7eNO(%1<;|uZKnOu2RE%4X@xYeHvtXWVXZG zT!t&@>{DJtd6(Bvam1@Af5kge@fscmd8a_vtV!>TWs;rO#q<10n?di4yhEUL#-tWQ z>6#FwoBglkfUfV9qGI@$`Us^q_`hn={eb^>Il^6s|D#gfcl-ZPuX~sO{TAKRY?94o zE)d3FbEt8_|76)z#}5z~P$6iwJgeiFf54Jul#c&xj%7$djZ6B&Qg{D*iy$Hn0-kK) zgIrKq-KA~R!H2d{w_E2qzD57)ALMTr9?bG9WY1(-6@tUws{3XR)b$YrhbjpUd#kRU zoQ(kG7_0*0NDy4F)$?6Q5LNem7dv9reUlL!xZdb>($!S~1j zAF7ZaI#a`ci8#)E&>G#3AwL{LeyECfd5@Nz_8y(h?%5eftMCCuP*rS=*BRT$e~N%s zuL_AFVsOUZC9utV{KkM)KR)Vi7?*If6>~d=B@3@;J<25ww6UwN0{Y~{0g>a$iQ^+@ zl7&O@rLB@p%c~`+P@A9)y7Rcs@)&G+c*hveJK>EVNIp5E%ldSiw;l(eT1xY_QJ7cT zdc@n7MrmIaJCj^f6Vo`s;br4)2Okhw)LFX$j~$Ky2!!g#784ogs=181q10+81p*CRIcZ|7mx@Z@WGVw zJ!$#s`q?zLh|i*-YOG=;PBxB}sPaNW)9FK|l^A=ygx5U-92=3|D9rXc%oll@S?SrOj^s z$y3N!-MZ(+T zf7&|}|9$TzjcMO5U*jEPa6F@N>LvfjN|S_CPZ9Drmcg-Lv^x_FUVohqgfM|oufF@e z^bTNJ==jV_>38VJ^jdv+A?@>JE6m{x%wdDKr)-C}XZCKd5gTG-#diAHI&YglC!4}% zXM4xWacuCvG97?667uEgeEabdRRUP-4lK>?-Oe1LAUKx_+L_*HAmr%fC1|fRcEY~4 zDEb~789OyQ5ySXd`T&u41J!#0R4Jf(%iG2fxlK?Ju}0~3`VZ$&c3BRf?GtFFA!wz1 z`*FSE0Gizat;N0n(C$A6O(Ew397gmO!6t}}YyA|tv?}J2)%spNZdDNvr;7mCEnLE<(r;-HUIh zg7AJ3CxZOJ8HTc_bRvjc(8y>|Cwdg_!v~;SS>m0EeTx5?G#2N8aEZBdK#0)6|5<9I zHdqtR@97Zq>X$Gfd>||28C@FFY5L99WaFrAW}|+Ce+hAun4_DQCdbfH5%t`Tvbi4EreE@AmzVL3Q$1q_dzt*L%zZ1h}r z7lEGv$iQDwox@+>qdyMX z(H{p$clc{uLX`Z6J@*6NX+Ju%{lJd>LY=hq)cQ$Jt)J`>`d!xNoRPloF4jk1tBBIy zIU+SdQrUg_`yL4Z|0drDe8@Q?4_)B9hBy7TqD|x%qQPEt`DfJF)OqxOMZZg(Cuki% z9-X8AiSuN^)zyC=-2VWTnBG4b+)tzE*Sq|#b-y{d&j+D;Kf?taUwu5hQ?l}3$6fjV zk~@E+b!M*Oe?17F7KF=3TH!~4QSn>26Eu1wcND>+f686og%^GQdhmTx@Evo8eU}d^ zApOSYj66*~^5YJFl-cW?k?#iYH9>@R!F!1De-7SzoihSpppDEsD;AivTuCHW}7 zL&2TLUfGT$z?H3@-Q&+LV>0|EQo~)sJ8Uj8oqzq5pZ?V^)i(OS)F1Z6=)%7vRbLxp z-GJKS7wZZK))qI_6%OHe=DNaRwZ+YKh1s>mFV_`bRa?BOuJGF0;$&Un^_(Cr8(lqZ zY$nmVtlHv#tD8vLP3wvi0%=`wO1IzSh-bIYgR>7t7cL{WOdDZv)^l;)6-Bl8B#Z0* zd?5EXxDVm}KKEhVTexR)-^BeY?g{SKa!(c4-En;p2SBS9?_rrXlV(xfP{Nw(?ig4* z^)0NL%{nOhqE3j;kY6(Fiv_e*hR9aCF!x4~>>ih0B`{G?@ zUyPZ3@w;dTF(t~rc;6$rdNzaNwHOp-Ypj7F_;l^$G4@)jSK^>1B>v>v%vexBAj(pw zcjV#O+!ip(!*ih-?wLRXj5IA$B~@Vb3N05z#=fY0WME3T3=sfbKF^yLU`gHNoT9qP zx8Bj%k~4MjFcc}Bb68aum>T~FgTG9Tuj`&^YLw?xhN*GNfP}1#wUcv78V}{vPQKNS zWu|-?HLBzCz&`~Ln85_7>;YXdTw$xxF`T^-jfX%xJ4%yjv#~gknJrlCcsU9tMKhF~ z9d&(fru^sS*Npw@aS!c?Y(##SXv`N_3?qnjmXt z`YmOZ5jv8EnxQ%C?LNTI@*e^TD=B_icU6~B)mc9F+vOBn9p8>+FRaf>K8>U3Wfa{z zbe4ZNxiAW;s!gNv8KD$cS_*Ak&=m&*m`ltsobhd> z4;E1&i9SGFUz4auw#(CIh0ZXPcKImumB4ceGGHD~{f@W<8KGTxqKTHEighx@27s>w zAkh3zdCwb;HnZ`Dx!XtBmn-!pg^5m=NGeo}3EU#8@(MBtswDPr!W-0y^$y}TgX63-c*t|F;_YpDu{i? z_43iO)Fun3mhDY8PMv)`Svayh#iimc?0oNH=X*Q3c0%59YYluP7^Uu+)H)VA`ZRd>PJuVlsX@IP{J%@69uxfUTTQV=sV0!tk7}&u z{=X!2Poqr5Bvs&H&#O^_1@7=>?oV1m?0uFH`=2FD$N$L^E(UcqYV&+@_Bs-Qom%T&wKDNCNupm+HRR|3$s-J=VTEW$l|?%H?k@3%Qsilviln1Gkn%b^I->bc_4=H)gzPp#jfLQ9xtoO)G zW~pPyuDb7De1I_Mp|xeyNC1WcA2Cy2T&6tK(yDZ49E7S_WT7)^kj*ZlPcqdRS;#_X zP1lJ5Frvs+8qi(xU}%={&T1_qVT)=lyIH+^aBUX-OhKgo@j5W=y)#^Jqy`<-y(+f1 z+q`$UtO>Q_+x(y6zY9I}Avb0Z>^o*x#Zk?gPB{Jqq-;hD2g4x%lGUdZhEu?EJ5P-0 zR#8TD)v?sQ>?YYOl!Fx^z6X&lXvNc`Zp~fdJ(@n8Y`kvn4)nBG^~@r-KUp}yKkJQ6 zwV&BYPlrEGw+hiP*YUlOk?_X*<#_uPUdQlN72B0;e1Nggbm2q2ml)Vb`L9wU z3e}f1-tUq8qTmPoqTB*B;Ee1$HL~+zxVMMFx>JMocn0hB8NqL3q-n)~_m)CODDss= zejDZb2@G6sVkShQB6KjJ39pb*S@yy`8cnevUg>q>eDpI$%EGZ7%Su^@NpPB-o`$m#~81TG`6MR zrJ|`%@x6Vhx3l#ihRSxVc>DTetnBgM4#)I=(U>mlVMINzGaPq1 z;Vz)SYGPtI}23HQ{(zR zrp8YH`^^rHC+%L#5M84peg;6LJgBPl2~}T!ynskuC3hwFa>VN_JMMMPUej&!T#wCj zJ=R6+L)dlVvvUB#?46|{5T&wr)~nE}_+}x%o&lsqVc78Ou0kTN5OwQ-*Vp2yC65S8 z@>6-;sSQgNZ#_+wBgi{(4eG=-sIPsoDvmP-7_{s!#>NP1x4K4@s~CGt!~py?jE>*Y zSZhYdlfc?*KGu&V3?e;8#i&N?1ODIRzb&*k!gEU<>{qn)`J6L zuZQECX&)B_+(uYzw|SbrHp8&T!#!jxT!TSuKmClR)Z!658AixSL%r&VxotG2I|w~F z*y639``UG|tanLZp%gG4&k%w?Wrnq?%H&d-Vn0P~ndbeRp+O8_xc75p>z!=;v=c7@ zE2noC-R2*K#yjY^l+y@2y~-hUD4h13E2a^Y?Fd6G z;~jQeAq3wb&XI06VW*z>rq8fL*An7QA`OlPex{5>tNpvfTa%(4| z37Uwu=+0Y7m20W8ER|ii+4apV&aNlbwWJzCs=V5XHuyvPGktVE+%B616@?e+HBTWsx>6Kl0qt5#d1qC(VLM6|SMO+afP zA_4&t9`gOy-fPdCGjq;NXz$m1zwh?^GWnmq*V?bOUu*BZ&N(+HG|WzRG%TEiW69Ut zo`yH^dlmZ}-8UrOKZxEOMTfuBT-1dsFQao`I{cl7L81~i*WBdyqp5e{cM!khiI-)A z`5*25|Kaw&5A7XufjHHMnf)qfYXbdmcVaP`@hPphoCn}@PCJALJ(oH^OTu%P86Cp^ zvkh4M!1fIdX8VBx=YFsiICmppdBz?vlsn%O0rw(cx$}Jl{B{Pd$;zG06cBbO7-((I z$BA@7=i?OM0?x;YJ#aqWMy~AZ9UHQ*anR}ce(Cm~j~|fk(DQMMCd~ObZMOF0e4L`; z^YM1vMmit=O0eJ6kIuZiB!AfRajGEVjRXD{osSD~PmH=gyD{n-y!|)IGp(!cuRR~f zlr1jI;o?Gx#D(03z3lkanM5>)4nE(ui?j&@tMllmIlyB;tMh2m!RY!9cJXnkYzYVb z>%bWW@|5}s=*ZLby+EwxQzygT6J;Hk%6K0fDD%K?Ggc!e;-3v9e)^69{!%Z-KMF_Z z!Q@ZpwB)}ANaxzQK%&zb4p1F)^>xDyzL;c2fN(zov^wX_2Qyt8@FTjCLmJ$dp+M^V zst|5w#U0+Z7UU+Hk)9mn4xHnvqsron!s^?rYQ4VOYl~|%Z%t)UXc+~OAAeY$hNrf z+^KrempE=spE`BIq>L$3(A~l+lhY?GxBcV;5e;8` zfE~l>!(XGG9n>F4j-q{|Vzp%aaM8Ix*%a7b$&!*xVdzR0|EcYWpNJ3lJw2bjaW z!8Z)5oXR)Sd1`Y|e65I&>R=nn%UNcSF^&TC6SwB`)EB@{V^Q1{nwAeVo~Kf^sUJl~ z@n7hrj#1TjADZ#qhi04w({~@-=F>mwvf{fBH=x4QcOPb74bv;9r?GZqK7IFL5q}owy8^cvs0j95 z8@PQTaN8BQrSHlp-oWh(f!nUYZ3a4%;tkxs5V-9M+-6{4_PhntHX5LDH6HaeV%HIw z%*6ri24Lc^b5V2f+ziLYc}aBpu(-t+FCHAZ6}R-|i)}#q;02DR(ROC0pU=e?CF&6? zeUhRcG2;Um9V76(<-H$~i@tkNg702*&pH1V>Tk}pPZArxiGaC{XFLrD*4O>PPjT#i~`SR<2sT>IJG-s9vIa zO**Ha#yFmF9Ahfu)r?~quVPGL9K$%8@k+)~j3XJZV7wfk_O1qowaZsW<^#{KPRTxeYwI_Og+`3E;*DM_Cwncv||--mCOkV2yQKUd;5 ze6Hk%;Lnvb{+ULfwd!jnnT>B|I`G$tmYm(#n!fwg(epb`{b}Zq!8PHdP3dhVu2M%F_uqEDBs6`wAto!NNO z=lizj^2y_2gd^^zr{b<-KE7k2qRnmGry`Xcy%q1|Zp^`R8uta#nsffm%=6nb&%c$q z@z&up8_&(5v|BLpXKuV5TLn~-Q!_W+Ir4nNX~)dOnddOh=4!PA=aBtJm*>@1lqWN; zqpw(BQ#MQRU6Zfqqbc*v-!)k^vhh8Hy@9@K(lYbD=30E$!`F@%sLF;kr|k7dF@*SQ zI|gKKy5)RF0`WBL(;EK-`K0gLGz0IF-`jZ7llat0&#PZ1H}1yWhn`mtC3{kKc^Y4% zyV|7m-JVmYJdGcA3?OsmKz>LTjUUe0=c z8p@Y=Uj?>lXr!%-@fDo&9LXIyfp9$``QGI46`b(EBw(WOCM54$oW6p?o+C>}P7>aP z+fpG+xO_Z?jxW2etFnk-IDD4<;U*ocanW04!b+w@I89iz2~qB z@qv3!yKltD?#{P-kACdlb6)ws;oj5bd-MZ0RGUZNa39&{`{g-z^^P&VUw-DUetXQ{ zo~JaKr?kI4PY#(UhwrE7JX-TK0sABzzDc;wOW6HA;rjG@e|zr#mq_#(lIZt*1aC{> zy&*;U$xXs@_cQ??P4gW%;l5^QB0ybkIc?yR>}SUC{oCQN&gbjjStKW~#G zcjA33uw85Hx4!+D-BJr+5-;v1dvV9(J*B|?m+o?rPr^L(Rzh?|(QC64t`WGyw||rS zuARPP_qp$SRdBTUj=|?`;qyL+xemS5Q@KmYdP>4tuIw#)v$% zsXWOL$kRI_Ppv9X!k_ZAopOvZ0+OSVf`$m_aFnOCzdcV5nI|*?G{c?K1fbzjZ{ICj zW3z;P-xD+dN%U{Z{r?h){uJG{Z7)eJs}+Em=TnH8`tZk8m+M(^M&CI0fqTlU;(rUV zU-O=*j;vQ@kL6yoJ{!H}8BA@jZNj+V&f2j_@S=B<=Z*~`PU=w>}G_|8Bh`WbnP zUJhoV39X+2?jMmiG$0X&(1g~{z{2GFx29h^cT+oEFp$SQE zAbDd!22Cg%nvnDck~h|C(1fy~2}y4td1E;TO$gQ|>=Da341^=u2#$Ew*W}-$QY-GMgyWO{_KV^z=i0-04Zl9*L}h&T8#KXt$^x z0I^V23LcaSJgnn-q3VJMAb3U!wR(*dNp_mkTR>&D-uLL z?vOONM0f!R?-H!6Vpl@bMtFeW5da>i;9-4x4b*P%00fT!@HhkyE8pv(gM$Y^LYg6W zAEHJ+?MUB=_oI=f-G(lAASs?=djb*(~Gg~AQ>IaJ|(hbFos zfA;UrN3OA6LV>Z#*OZB6rC$C(PgD^qZ1J|H!;R{933P`<3`t#)L5Z$ z`zPTNp>C(|6v}mUJhX9f*C$Th4UAEDmqg*txc?#gGh(IwOpuFg6H2seHqCIXK?( z{RA9Ggt~ZED7&B1Vi$dql;qEavfHJUUE%*rD7)ylzjYru>ubZ}xZU^5H{8{~N9+8+ zUHvZl`x|0O4qo#JQSgDg6H5Usw)gRfvIWXGlza5gokEolR=`I0iGeOfBuBLjW<*kPf8+82Sh<==n?t(6c-X8D!)o1Q0Z;Ai!T4~^knDD`wo!D1s;QpR}y->f=kw{f5Jm(pe z_yeIgL6?KB2rWa95(H_XL()i%1My}t5Fuc1LG?N%l@vHoZcs4HBu1QqqXeBs(Aj_mERH9%jt^m9TlC>CnmOUqDza7ayX-7=tuWjBsvT5XE>9$ z`Yzzp!ni`y{zEnxC1xdd?P=H-p^gUr2je4*-(`Fh;ZgUgcL2dReKHyt@sDL}K!YIn z?TnSOfut^E_l=B0foM~y_sE8oTFIDB|rYl>qdZq z?6-?h^uTxjmY0^5d3|M-FyC2IT3h0Uu@0R3rfp0Z$b5n%8ChI!Ch0lqP)1G zmaK!wz{boj?SC%Ru4mHJNy5%2IXOSRtkhRsR9%3N%;Aj-gq7vOc*%=5E)-W+6qI?T zr_r#-%C5%^L2Aje#AMgALv>o|W%{4M14%xG!L@@22m<=4e%MpHp6|(e*C;-!+A(Nv zGk|c^6R0cFdkr4ucqVaxE@XtVn1KO}Nh&IVu)~4nRcXZv_6@e{d4~gSXZqD@U}bl( zh)~`VScVmOeKj@2;`L@{=4CQ7K*=e?5RgSEnDF`+)8&tEbwM>n7#!bb9!RkRB(ASG zfB7gcC4rrG)8$neW8dKT#LMMDQ}Y0^8M12XBSZ_evoe?W>7e}eL%X5+t>XMq9L6h> zUDS@CiDL}nBl*=MK5lcy>}sPxv$Bh727fYW&6vnGBLod^^YNpL<~I7#ctBxgc-&mT zCHC1iMZb#Xv-EYGQwuw(TNZdWPNzuD5B4;^?%C*xBcu2G?m5l$Cd^37Ng|wpRw4BQb z-Dx?=gqdkMBM4`t=_k{h z1+k&)eC?H)cn{fIu%!_`_chnOk@)<5*@$YC_7&z;Eg4(Ozv@Vk{25Vozvy@tasLi6 z;bn2scZY*~PzJ5>HTKJH6zyElH{Q6XG49Q1M0wsJJY@d4v1c}XeZIDQPUFYOWxvdq zk(_p8bcn&uKh4r}12% z9vfe8$PoQCP|D0{Z<+h+OH|Lfn+p5oHXaeCN~d!h+e8A?S?@-^avQ&*_j}~R2Jh8_ zV_@L~QC8@velC%M*_f z>BEGPs=vp`{yyh?>~E4q3ZLgT{w24u`^LsrPs8@OfFurrD%+KO&kU7!!F)M7y+pET zIFGi$*@Zm)B9axUNe_x7eV<=0zoS_V@3j-b8u7&hq)4M*wtRPuXVdiGAb@AnQ$yo&5g~VjL@7R^j+-J0km3QG_&$^2(enx z$rg~V#rRo0|Im;1vnvEwX#U(0=v5Ku-vJ$EOuY|?tPZ@3aSfxF>EC4cO^mY{$1{Bt zyDw!N%=k6RcM0M@0z^ao(S9J(*|ZM`J-YloAXK_dZvmmprIKYa+?V_Z5IS1wqric{ zRX{{po&>}z1yj!=65OVq0%EO`dKd^DF|{3tzf1mrf0W+yK%)NwNa_3ph^1QUkJ$fV zAXKc>W$d5FI2VXlbfu00QhqK4;_s3q{G)U(22%cGffSz;NPK6IC>}vc{haYVAkm)z zV#%2LC=jmVT-^(G*Np8=%wCIMj?nVJa1-;cgT zyTe*0^&KFZcIw?|CnTqpK$24~kmNKQNOGV7o${XnM7ozCE)fsn7Wo(hL|^Ee41~@o z-gAiXeUUe6d}@0{_`@OG(CGyo@rm9=*edyBni;n;)-&cax*3xh0hCTm10{x^x}w;v z`r`<4qo8Vi@FBU8dK@Z~u15SQyy~B=OpjvvB@iCyQL2C4O>W9P)5lYI2u11lhbTOF zb|6iB|3(n~$so7pT~SddRx~a=V^4TUWAO7?Hv)gQ z($H{mjkvC$y0GN>c`Lv0X(k99mhvvt7z2g?#dSmv?S;-6L z-JCZwo8o;K1F#pr);HzAk#V1LG7;XPIG%fi+jCPXo zikqc-9^(g?pz1fcuzHu6jEREWvl%NGuL6DkSeG`Rv7B);<1>u^G+)!!?u*fWRf>Wx zh|}&vx$?iGX&=J9w=zznIkn`snzkF|X}&&M+XQ?%E?WC3;}kCEi@;oj9)WehkD4^ho`hlP%p=0wVN4NF+R-L%=i^!BGn`Kr!X#K{Ab47INm!L zN!OxuXpSUoV{~F3BsV?nmfVVmJs4>ZoZSD;xSjDh<0Y7jiSA*%lksuJeT=b~*NHEk zv6%6@jIS{Mg|T<66BzS>+weu! za>fsUT7pA6%-EZB5x7$rXECl|{3k}8aR=k)!1f^yts4ql>u85IAVDIXO_2K*#?_2J zWPF)%KVwuM$$y^Xy9Du3yw@@=V!WO4VaAsj|HN3&<@*6+6XUClS0bH~bcZ&9aTeoZ z#yZ9sd8n6Ov04qI&iE(B{@;{zH)9>+&lo>sOuR+%&0t*1_-n>?#>*E zizME{cq`L4Grqz26qLKFn;hDQjL}e9$vv6zTa5qC_%7o)#>r6liGKy-_ZZ(|JjXZ@ zinrine3(E|e+{<{7@t;|qKVf{HaSvk$<5@;0%0clBXS|*!4vHEj_Ve%CQBW2|S~%J@3ajY;i~z%iG*w9|~k21)ld zj7xycSXkb}_+!S`7(Zi-8!Y)wA|LtJ#cBflQF+S4=dq(+{3t)@pp{$ZgI-zQO3T=AGyaf&SzZ7*vPnz@fxJF?Yby!HsfuK zO~CxDC~XyRvpY)r9lQ53Ze;(X?4CJ7!>LS^mH!p& z83$i3{nHp{Gx`|6&G>JO&ojQq_!VQ{D`Y%ZG0tKvX1s^-M~uH^{D|=+J#tx3R7s63K#xQ0vHc!;FuU18Czi0e{F@BQtUzdvc@Eg(EcNw2& z+{@U_IBJ~ao5NVn_yfjf#xEHAjhFnB7?(2M&!{uL&G-dlY?=%=p79%us~BGbCKpC) z?=zle9G))yuV-AzxS8=e#!ndICrJLOj7u2rW_*J2L&k2#;gBDte?8-OIbXN2``e8F z&NzzYd_5yxbtdX#9pk?+KEt?&@g(EW$&!Bx<7lW~++^54R^iSbRwQ;cKsIbOzpX8bK<2V?&N$>(8Q!}u8E8O~?l zDYBfS7-uqi8P_mw25!9v>s`i|7}rfj|HQifUm0I!><4`KeylUu{Tb4YdoUK}&=Qjw zJ&dJ{4>IoI_@9B>^DnX5`;1+TmljI@qs$kdf%Ui^tqo@!G7avhqqRwl*RuaDjFpTJ zFh0rH!kEM1Rx-A+|6HWI>)&IwHH=#s-(@__IKD{oEo0owct7IX^-{Ff#P}!1m}{he z8siei&5XZe{0rkb&QJeqQ7_x0wM@oZ#-A~M$QW~-m}|;~6skZr`^x^7~>m^hZ*~4OTMX$#f+O6 zf5Et$@i^nfNQdITmhoSK`IpCN%h-Jf?Ppk1>v&i+Wrfqq!OL z8Si8KG2| zeT?TAFTY8K8@Wu=v}Li{drMJ|x5jFpGoE351W5C5KkyNKcLB!TyJEGc*#Aw&ofkuH z%N*KAj9)OGV~p=F>8Xrv##c_w&Ug)?LQgY7&Wvb@*l)_DcXy>AV#~I z@dn0ajJ1q+-VD7E=L8!Vn~={j*ev}m<6g!N#xson#{4Ps(LY;awCfn3VE=fyDV`VD zJ&)-_*}sMTpIm@(|7?u*2IB$7KQa9vV;AERjwcR$l#zPrSz1 z%Gkwt(KjSLm2nnh0pp#F-)H;<;||7^h==5GH>dYK#vd~}@?<$nhhjd*`A-KqF)m|3+;EuT`}4%j1`REWqgv+jrI(C zReD=wrMG~LehZ3Ml-8tA$SCtxmBK#US6ox;H6IB24^@Ns3;1=D*0B{83xB)7OvVXr zZ9!p4G0qXKqv2OuMj%IwW@Yj1VRH-m3Ob&(&O~`}O-;dyVrijXUa;1VucE5X7dDxS zn&Luc2*|yra(Sr8Lvu1=@{|jyB4W3#AtKsWkbxRbu*7CxJqBu|NWxSO#bR4GMC?J; z5L>vK5%i#nAwSD{F*lGJ@U18;w-L0W+Lg~hw`hEd8mWYe&$ftuhCmsuOF?EAvXPq` zxU!o7JA)`r=-@rE$>uy{Qx*##D=r{ytB0>*y0Hl!?~ z4y?v-8&=DP8J^ds>DUCaY?|SDFDwD;+OtW)st#>f!`GxuAe;KM;|^`0b+sjdffNq) zer{FkphQ!b< z;SoSQf@d36TI*RSXj33V+`-jGg)oqHH38N!N&VhAT4qDK-C^MsKR zaYPmqafO!@aRdp=R~bDCcC2m6utE%8ND&D`AD$MmAr%)< z1&5TYrntB$G?UhH30WjkqRP{<2*m|8fiaP|$-|OUNI}JAlDvsxA2uW@5zaQfnqtGq zDusuqohghdabgPf#BLC#O$a%b#&jI=LyZJ)31Q!mwMm>>lu6NY-IzZPOpZ zBxKd3!mwNRqHz3nD}Ji6FyUwNAluc)k5axnfi(!aZ zF8hLac{RU20H*WoRa${@91jsFcdZ(3S1?0C5!tS@qo@*tTd}vMu%N7(lw$D5d|>qD0)=>aD~eY@bqS4~%*c3>{(t=TH&p!^R1|^tiyu~DxRwJs&p^*{HmY;h4@W0_)R3P zLf(_}bCPdRp#s7Rgbw(5eFZfJ=^;Y_E`Lql$Wu+B z7!;vt$XfTiP0~}os9-$bz(L>;O+mS6YjGl?UV^HDkSSH{R6q_V0w8Y?8=u<{4FZC3RA&=F? z%Y#c}!zD{&Lo}8q0t=PKh9!JfF_d8yII#!WRh+h2Rtyoc9n5Bvb;V}LzOlNvVVUcY zJ%elC?`_)$0~~e@J-}hz(gSqcrp_!GmU+&Il(!5FT-JSC#T&Xt60O!xj+C)TIf#^j z@acO^$>T_=##>g0$(MG}$su1o;wA=DravjRy6 zys@?mcw?gQmX|`6t1PQ4uMpo%lUbEB2t|V#0_Re7WyPlWEa)O)ZogQgVm}7c1?dT) zerjB?qhPfcSZx~OlHri!z#_p&HLt*p6w`=mB!skdE23?7RA}2AfdLU(iXe1#X-wK; zd^VDUmhVQAudHBY@dRHu5gR#d%Vvtlwt$LDNy4(I!Aud;RI&9WSv&cvL@`v?RAL*? zJoQ^i45LVpYGTP{I2}@H6jqj3Rb#1yM?vv8D*O6x)TWu@mW|QqqJ9=&C%Wu;$dFg% zKZsscP+b6R+Y}2Gz=GfYKwE{j38-3`We?^?WFNUz=azxOHWK?zta5AHnT-teeMh4( z>~nJgoJt@ZnQ3?6sl~##@MLzsjjOGF;U;p&WVp9J8NjAt| zjYa-qsQ@4t#~$U4$=5CkknBmin~QL%Tc!$6W5cG5gbt_VTSEVgLuyD?EjWU*ZRC*ZvPnEn9hmdWzp8mSibW+Vr-8Y4-an<}-%NK>a&k#!iWP?oavM`^A+tMEbH#*)QW z%>H!3%G=0gD|lr-7eU7UxWWkA&!eQy!=q){iYhBA3POx}RXtU$5n3s~{Mmk4V|+U_ zJ}qBaeY@P(K@awmu-q)Y1DogUK|(Dxd##3(tT&{t6bK7dq_PMnF!II5Dgr1W7QUtz zREw1-r&m~u-4nj2Lirs^N|IJ8ju}Y9LF70EGWk{)tO@u8g)6cS$lk_~MSe2^wKglO z#0#4(WnOCI?JX!Wl_Dwu(2MTu3`xz=14tCv{jofCEKx@N-X<| zU4&tRK9)g(h(t)he`8>-lIL$__b7SjL?xfnHr>p#*+ zOmyoO4vJ;NhJR3d4j7O4QxABtJqh<7qrYi)%RmSfhk+<>%^5S01NKUEvXP!j7y;aA;cKZ zK(e#D3$R$2*ae6tYr259MO*L}&|pudYR69tu?r1EX)^l^cnjmtFu?@fCakSZXCV4u zmPpkT!XKu-taz;}FoZKqjfV0@uHuj|Ve34E+olnO@Y&TohNj|za&PE^9zoCI$ZE0W zF2Uoq#X*B#gdntFhM{#?;Mt3w1u$3}LL*2*5JxX|5=Z3n?=32Nk=dvbxr^l&eubK|7KG|IlXQ z4-(uW`~-_Z)=#S`t)d)Em}Hw`fFn%Wex`u*N>>ETOlsf=PJp~Z$7Avpd#>1#DK>6D zh$ti_NS_fD(>9^-L2gZZ*22@iA5g;P@risaL?^thAGr`o$d(o=D+))?R?y5Gnz7KQ zE@Wf|+YU7eQba*Jkb+l=#YU#ZMbZ#ANVB526e+xH6qk+Jy2ydz2`7R8Pw-2|S6fmG2 zS{5@*b5ik0K^fywJDnzJ_o(TF_70UwNN~$U0-fEGEcN*kTY+1Pz#>wqBq+=9$R`^- z@2CKQbb=zHr+&i{$(}bN1AXJsTUr>NFLd;<$V>29QcyX(;wfeOPHh#Vl{#m|hcbjO zK!id@EFTz5+D%X;+Mro1bU?JI++q$@sN@-s>i)l+rP!!hR z{iGjuA!-UHr1iL?Fn@5_R5W&FG|(-oR#79BS@GGIU2#~{tAS+E1=TVnxM0fPVoA+H zRq49g?{XRiD8T(HRSj_o-tU0Gl@(YVO@pUn? zim|p**ukun*4o@VjGpqa(ms$EpP&_wPSwJUyr4QTKu9{VK z#l>_&YL!EaiQW`gP4lq#5#^Ltw?-F^@aRK2AztQ;naKVenS51J!Pzw0y8H7+PfZIG ze;er%J)(4p$y&OKVlIS5&bP*Qsi>k2TZs6=D-6;%=9#r0wB zZ_aiokH4*il2jfnOM;5GsUB()Hr*hQ1~K)ldWbVj^(eBroOW4`P(2ix&FEv!x)P1q zH>i3jHleeK!6<}Pl**j#P#%lwQPHYA7(S0>{?3&6HkN@(--e>~Djpz&K=IWnsKrLI z)wi+AtICS$1Ly^{WT+-TiDe8U@6GWT7S~4Bn&QycyP4?p4NX&q{Ld8gN4V%&+9{JJ z_~^{XhQ#N0h$AS%Z&po25cbF zqh%Gam$9N$m&?X)OL#AhY!hF=K%zm{aA;XgioefQbZU7$Br2XtcB%OJ?5pIcFD|nmXBOZF^NnTR9A~{0!lB=i@%7=tBZ{d z4SROfH~nNhBu^>$-Si-RB|14LCNpbvnz_M?4Isr_Rb5$Ij7OC#DhmqnNx?un#G_7< zLf^fstSH0Aa&Q=;tU+%HWVXDn!W4ZKeKL=OVZ<$5Y08RtOrEKA6-M5&QeSleOzvm| zL-G7^D1Ye1!lbsm;9In4<~TyAr4dY48>{!9mVky5dJ1x#R{EcA#8Ydh1fZKOm(PTnF0 zuseQ>6wqR|7%7N2?7ZOGF@?I|DiWbDsGj^GE;uclP3lNuvDvqd9L!=@zbAn%tT%rld^B=nUqbos)X zu&MN*JXSANQ8mta6;q#mM#5B&ZAez5ErOa}BoiWR6E#w3o5p8~!=`vS5nTM2vALlRVMV zS;fl0z7H*K$?T)JMo{pMrV3E;ed&M~wDTG~Awl2JCT&e|8aSl7UyV1q(*i{?kn-$> z5=vU5$uF3ZR&c?L^JnB_&zj&};K@V|{lh?*QuKsJ?%i71xXaQ9$wkQ5^h$&<>G#MG z+cFuy(Wr&FXM_Nu#+cy)*xG$s2J@SXhOt!*AF!t-@~32BrX4wGU!w)MgKf*!Mm8=ZmQcxAL=;Fx8rrPEfgnDkfcLIqs>ajLzJl7qz$3nF<6_BV zZH84`WX(*)45V#3H#fXr#JoX+hqWzEX?Y5@dXhUNxb=B*AO`y;5tKu#R}Y(0%djb3 zMwl{WSk##*VZmU3*q>qlk&vLwg~`9auLc*uPq59UiNhkB0eU1s2RN-d99s#fO)D#j z1|mR{5r_9uUzQqH&FwYPX=iQ19{^Gx6baeh3!(?VUc{Olvg=~Q7BY}V;ENP*@C#0Y zqGNIoV+l2fw24lYB2v6q4i>B~C@m|%duj1Jvi~DZcy%IAyTKo65jX`6&^`T&#%~ARB;$u`2!0uYDx4wz@?Fxa zoq3cy7T)w@G7dTDoiTynUT@yaMfiSK@YTujP<&1hJ;+dhVv5r03Do8F;=%R;<)ZIw zWoPDP2Hag7PaJ!-A|sR+eD+foHoz+G@SQF?lEG0#UB#MGytT-ii?2!=F3Jq9e2%xB z<7-EJy(Nh`q+73A z#1)1A^drAS{HTsD_;S|Y-0bpxsA=`P1uA!x@<01s@^`==aSu5Ey7pe@JcoX6ol`&8 zpl$h!E4r!E>sHPO8-OHgpXH08bZ>^isIk)cBZOM+yP)6;c;QW8D zUOU?0)bdK4F(UtCkbfO{F43Yk#@(&G7mvKpMZV`oySon}@0}=98_K)i)gNVyYl=er zZj_-7WqC-`o|Dy8fAQVOAC;%X`Qsm=K10g0gXL9%Jh!_HGD12eqc+HIKhik>SyCD@ z|MeG>EIQ}?4)RAGWNBN_jyK04f9MOFNp4iedQF?8{G}hsAQSR@Z5zuHY~~C6bfJZr zFZS2$=+tKEvZGTQrpu0=EU5PMF%jr#OurL|E=b+Mbp4!N*%CwOUom|H&?@{|?$`L# zFkKGXBWcw#ud7>(VnpyM{CnQ<#) zJ!3wjn=zRYAi_DFMI}z>F2^2+)4A38nA7Rp;Cj#%=WKRu$N#;qk9Y#;4~h0gLh35T zeP8&gak>xD4enTSQ&=Ef(cr%fKUEJq;g<-vYIh!Xt9t$fG@`3|O<{T(kgn&MuG-BC z_z}OlZfE)!_8=0`li5vjCpTm(uKU>?$L?=}hClx`CCL3RLGEoq?#~g@7~Zc5o9A(R zZ99n1MgdcwqqNZ;6**)b7ZkhASGEG0l+EXz0u1VDKUP?b)*OBX3X?pmV?oK=KLQ*` z!1B#_0kS`lUTtpoiVL7@a;!6~^j)Q(+SM*sOF-c*t1ZVz1?Yde_^+C? zW~(Pow$>A;rQ7O>s?tefVfDl$>YK^3iIbj4W0&1kMGUwyxIZg5)E%KCsR6sEdSX3y ze2R~v+=g$s{)iF?(G$yHAyQpX22*Leg7r=nPqLtD+6z$AR9?u7G&kcFN$=ds`BLM; zUc^Vk*yd#aP-cvdnsivb^J$K+nM1X6d|MG8bP&2&qS6sb@7$HFX>JZgQTH36Y4{8> zzLTzLX{bM=-kAV@`cbv^!B35Cxb5+Ew(>Xn=@kyW%j?unE_6XRiGofO4V}iZ<)AC> zy&Dd$`);Qz20CPP(`nKt8=QJuZ-*YNPaZrEohRC@?@w^+pB{+bf($(LW2fWj@dX#@ zr~7N&anNh;iPiFYIoAE^2FHMoIhSa8XP~RybFupvbnQItQ?0Vq75`+bD|$>dX9t#IGQ?xAomg8No>4}p6j+>6+qg!_ehD|AfKD=CdQZE#D9mekVz_+Y*L@x^+a zV_-{)V-VcKbZ>)pbpJ>#Zy?Hg2bAkWk1x{?j_s!(dVDD`K|l0|CHl!v7V904-==qX zweEvs`+#;UXomBF5@u>u2B3((9f5TkduZYS|B7C977Hw{5i6 zeGh02pdCk*D&ORWGCyT&DB@m4}TZ@TT!+)E?fKPx%yD8SIaaluBCnS z9Q_B*UM!)+@Frjq+TLLFRjh%ykH#H1pLh7BW%{9~mg=WQF9Baa z{a^>u8=t737<-$3@P@_k>kHZ5s(-Oyk>2r&ySsB=tp3GQaj2UY3Z5_3#;RrVnG-^|_9e1HlPm4OGelP$ustef< zs4mVPnT<9@;Z*yV?TPviwS`lkdqAiBP?~{0KyBqD#e4r8Zlf~2_Q!Me-l)efpP#E= zO?CNrj-Kf1)w0kP*K!c;cFf%x>d>*}(0AP3H#p)u^b;iiOZ7wdL*|eV)#D+^;2>mi z=<(tD5!9uuyP^8=KMc`3_a*5a&kWY7ZBRX`@pZ%p=kxyf_4&brF)p3`=?pz?mA0jQ zqt@NN(cK;A7`(C3HF)Dm$YoloTc2Ezi7^o4)f1@CXO`%PU%FL4^7?K15l5c~`Z@bP zkm5*e8Sm0C<|Vewb@XeQ=W+lOTWEZ)bZQT5b~+wd3HOuEJ}oO9eOev_PajubV4s%M zbNTPzgkP`cu1kr3ae7KA?jOLf_s_0RNziXd`R^awhhS_U+Hx9w+dRJ0ct1=(fxVG- zr(?@0w2{jkgF1ShJfC-KfA_Kc-sbtMeXzUR;TX7)+M$}q&O*LjEAudJepAS|9pn9> zr>4JXFXTZ9>Sm0`mP1 zgYiogvQB_3NyhD0I9~i>wByBtV;wIZ9`AV313!}Wp^1(c+ow8S?6?-R8yqkG_sctp zV2%9hnZ) ziB^B}D_M^B&aImXy(Sy_O%}%98PIn!^|O_4ZsR?ehr;SW4N?a>4jt$Obf5$M9r_vU z3DLN57X3U#4@z_d^q|5Rp$Dyp9(1O|sh@=&bYk8`I_W~rIHe2q@6a#R@_f*P>iv39 z?(v{?x7pxBr!C1c3pbuT5uZKSL(Usb>Ge&E9>!xeZQn{(#_LFXeavVfCgnsmi z7djE~`XT682cPH*Kg>5z^wCd0b*s>iwgu@&hi=fiJ06b- z(UDF-Um(4J#^|njq$AOs%{o#;lzzrb^kGfiLeC+6g!BSAUX!jO=X^CkP@9wOup2Zn z-xpGN=t&r#gXf7h=t-UXNDors19Lr!Pue$AZ$>%}*AV0(AiK7PKKg6$cfnukk+N(` zPa4a5(yyT>otXCxy%T!UDag>QC-v{xd6@Mi^qqs~I|tEL4t{c((2ov6KBPaLd>?Zz z$|mRE;b?=TD`DP!;zr<5w8tSBOE9Kjtde?D|Bh{Kn77fszeM{!^psaWJsRtPr~2v# z&mbP?P-hn})w@ZbLj4_Fuvq_M9@;kAL&xvk-N(nr>0eCkg*xkldgFDvIo=>3NbV7D#A?GvD9Rj+OIu4d8}uis15x{< z_9@3)YM)YfqP|6XF4Yf@MJU%<)c=_*Y8%6vxPAy9ZI%U*}i2v z6KfR6MCwq1Hfp6qeTaI#%|?f6BpnL%`z7jE=uv2ohaN{ALq9(Vd5AjhfPM`596UZ; z@BAEfi~Jr(-F7@9>z3LO)h~_Fj`z>!wLq7;9CQA@L4h&tDV)g+!ua<68g0uq=b(*q zT!S`VmFm{7#@da>>ccNVhkAXP{srdjUdZo}*B9$YH!jiBorw?3(E4FchkoSLS{4Go zg}FUPbF?gS^lmxMOSWry3fpQ~h1%2&*--nj@fRLAN29wnL}g0ljJ`^s19+=QlWp3BBr@kSDF9J+6Tp zd!@Mb-dHP;9KU$}Ht0*s^tU0Kc1K*xm#-|=zj|wlPP!?{G6nKYfE=d)=RlTMIUOxi zAtxorQpoWE$T0;nUhC)up2U_Rz#Why$?{|tbfnpi7f8#> z*?L#pMr=+EpgosN+#g_lr4{=K278Q#d}YzxX4hOMP3dt0)}JVeN^Trt?YC;YJ|-p6G8~ z-j|`DNN7GtL(XTd))bx04`MDj<(+M+y_4jH| z)+f|Y-!L*tk4vuKJ;uHDSjpDrV{wnR9gD+!qR+T@_rWWlIM!>z$H!9UeQ_)W>$nXg zqxEmSbpP)7pEez9x^eHZL!-Vv7O%y(^d5Q7?y}LMEhVQM=eM za)v&rkF#ZJ9~b6!k`?W<$UT%#d2W;YL;rL9UqPQ}b)l`IPrQmf&J9=2)E^j^tv@(r zmj2JzXX%?~&Cu&(GWC1nv59puku*d9jY&kCK-G#jpr_dKkCrB!qt!ruS z?z^YByX78_)VJ$nVj;tskl`$`mZ^`4fjb-Sne2{+I}7e?c1OWI1MVz#yWplen89u* z+-|rt+3mo+n{_Cerrhft)Ux49P2X^3Hm_~^!%chS*(ggE$}|II%fy=UjcrsH=U;cc z*X70j=)x$_v5&qk2KE_LRu{%2C)&Bzgt1WA3Xr`(`(W&wi@k5{$-&t3#yGVFZE1_z zH)tR2)(?%()XUJ8Dz4Aet7gsAW1YQQs?e_TQD&SHJTOAL2zv)|-yjZiAMMZ3 zzCjA+z>4c<>+d)c9ysji13e=FZL6QyFIefgsKvi$Fi}f6IMtEj-z&JnnbPJQ@OH;& zR|<6SjWkZTiM-Js8m}{YyY&P8F)#BNM*AMUP&Q>_LSuNFrsbVmHw&_u37KR=R#~tm zarkXX_DxUvX8CL5k0xJ~KBMNjF&}FVhtm}u*Jt46Q*YgT@OB^4wY;K`5bQdPM?r#* z{OM}8p>M|_JzYP?p$X2mLc&*JmZFR7oUG_YgCzY60PF}t!p|Qp>2y|O75+{f>fp>O zH4jL32=gT_^#GCO)C?fmBa8v!j4JgJ#D}w;)U`n3ua&qo35cwwMlp6GU*!IjaS!8m z#%CEHW6TFq{B9gBQMzLo`!RMQGV*U_+`;%XV;N&MqjF0BVrIhFwNm+Ha4i-1=Hajuuz4+xzk z^#yn$YpIVhKE!x0V=ZG5V;-ZMF&~Lg_*};87_Vj=&X~Y>7MUdecE+D0^W=Vrv6gW@ z<0Qr;#E~UnTqNl!t$|wIF5|=gu zsUCk0MAJxJ3#4?481opj8Oh*^=pz{iFh(&Jp;1vj;()~0DGkL^KV{s*xSjD?#>W^R zVk{v=7vbkKrZEm+?83-E^p6;u86Rfcib4^617iteHseS}Dm?MElA;dW&iEMP2F5bR zT*fp;tc66luP`1__&tnQGbS-&zgf_`FnkeTJL6u)9gHt9J^~~;E&?KnR5y_7jqJ04 zOCfF=98sm>e7f&ei81wz`HXJHWJUm;XU9aju5)KcxprLi&PBT}igIngc*n)>TpZ#w}`N{6#+RQ;7s{69M?%C#$PPukwJC|BpWu5o9_30owiJ<~4`uhBh` z-D*DA&2BaRH?v#KCup7`yc*vxLMF)`O4%oj3UUu-_vR!7#MOu&rLX1>S}>9Ql(I)S zPHqTH*%Qo#`!WN49oz`+zelqtX|Ux(w9ulEReTiX2KT{!eA5N~10O7C(QeZvHU(sZf8=tKCb0&C|txuV7r9pJ| z_CB_D;S)fi%hoO&=InlRpMThelaO&mvd^KxiLQKZ-0kR7_|yJx*oF5)T9`Jq`II0o zbZ7A+Ka7cgon1KA1oE7U?ZTB?p~iOMa3hI-*o9*s;U9M4XbF5l9TgYN$GEsRNC*Ar z>4|i1xd{$xqke#-8C>~3Ip9J_N7fWoM2 zT#)<0Aoq`g+^q;`4DZ(*0w&S)Seos_jb=8#Diva;Op(|dg%dT5O*oOmnS={E-sfa~ z-rStEzt$j};*MYsPArzHq0k(OdI8qp#Qs+sgHzy8RWe*uK0;hfE^ykECbTb9`Gx>abJi_?i(Pwgl)h zMLPi=GzOAPF=FBeMqK0YqdbuvIgNP~i1?CVKTlP&2n#b>vm5nHx~CtFJJ|M7b2;vM zew}|k=Do9(1F?@i2z#`Hu~(afJ=-DJzaEPH>zFO4VgK}*SA$I*ZZfL@E%^2i3Ym*Y;@f)DLz z9H`~ZNQC`Ul$LjXeILCw%e5ub0UL1ntm8u`7T>5pv*31pYwTLsMbKHK+<)F0I}Lto z^q(ZI)hVs5u^Di$hIGF{+vrL9 z`Yi3}Jwv5EcW>A%HI2R<_LlMb%igv6ug9*@pBTMb@0dq6N^$x#E9&%T3u|>@tC0oU zN|euU>pC_@+PaRW^FOjD9SpnbxhMqB z9qrlJKwI5$=zQLj*WQl0UkjV+c>RTyYxL(Atkz$eSEs)?u@?3TF_076?%V)dDxAOL z%x`0_5pH3#Pxj$tm)nW5o`UR7L#ADe=Zd-~dn{^$t+8=%lYKnd6_d@4^lx*KO%K%v z)s>YUGqo)xOWCf-hK-(;B{`oIc4TyJN^MKZl5Flp8;G5ae3R{uwA1fE*~!*?2;_M9 zT92?--Zld^1+Xi)!O^>8XBWv~8e}jRvdDlOra}%=Acx7217rtTE7?)s&V)_W)#s%R zGMy6*8li330K4Oz&cPcex2^a0Z+&c?{=~%9`jgk9e>r+TK<8E|u=RKXw)uo9u#2TL ztlp^mxp2?L85f<8=Hk~YRZEFacclD%#)u>_Mx205+94BZ6FjV=wEKMC?I>?P&Vkif zM&0F!@^K%X%V?Wu`V9Z?L?hZfpvn`!*>|a1*KlOha6WC_c zKlam{Kx03Rqp}av7%Tg}vJs_uAu#sKd4a|m8c%3mP~%LLeq4;H6t1UZKZOq+`;Q|p z(uOF|Hq2u`jd6iFy$xf3=R7s`_s7^jphb>La&DmUOpg8iG4>B=k#kgF>?gf~#{d2n z<3Bx9A;x|&W?}4KU>N(y&%ro0MSmV+|1%i-&mPItyU}k0?IFHnM=wo`{Y^CXLl2;_ z|JivM`@QyKe@V#Lk7r#_N1YMJer0c|>V(FAst+~x%k~j?hDGWXQkHV;CwmGxzXy-~ zQkFFKi#BjUV}E>L>|f~a{>|f%9VlZui{c8vodDYzKc2-R>jUqRl|xUpHKPHVJ*a6S7^4{{9-q?tQ}oehOPaH$qKss3Ifm>lp{po; zyJ>tK=B3s8FK$@lw;`eV=K++B5YNqHe(K$#qih)SS_Y$Rep|{bhNqkweaYW-Zh8S_ z+lI28;IcKMY_*t=-F>t?oW(wSetm*I1at6d%wMhe$+q?=^Vmw##P$TecF)rs!&>s7-_=2z6Qpg{V24WR z0J@_bPX^nedSK%oY=>Hk=LbEoSuKGbY8>oPx4{mz9d@WY@XSLm*rBGt4z(0^sJ&o^ zdJuN!9@wn5B45(hU(OS9UXk{cV*MfJ8M4=xx`L$Be8f73pwqJg|L=vjdln4k?tyJ; zmZfc~l$q4aXzl%f!%a4(^=xDMD4r7w*qCO)c5?=7Of!W&W3YXxm2IhUj0$g8Is^8h znZhoV>_HzG=Z0P@$2+zDP~la6D1FJBWx@N`*^gS=43jOTv>6_>WTvhqySodnB%4vv zjb$C=$3zSJNFUpT=Ep?AJwx}gO=y0M3vRr_ifuykW1Mii;m#EML-{cd+>=eH90#dh z<^IrsmJ-;62HU#w^Mqj2w7=G_aWm|Ttn3=epRR2fAgt&abij=39c%(x(W79;M%Tlj zW6#mJbt?QHft?&(bgp7WABp`<{H69|BnP>_LO8-!#$AjL1CgcFN02VEocbW+os4CS zOBiz*uVW-5J_@%No1ug|7++xA#`rMf2FA6FC5($0J&YNQV;GYd;~2Y;X^~$b+U7D)0cV$5UA zW+Xd6qK{-u0Af!!wF`++ypI6MT?>SWQ;XO?j{RQ%k-|U5_z>g0jJ1qKj2~g+nD}=v zzQBlgwh8}-88HyQkyI+#2~s*=VN-x`7vr;xk1*cLxE+-s z%E5RqV+o^&aSUS|;}@tj5kKRzj1MucWo$=fVsAZ_?EN6(vRx9FJ_v*;Qx~y+9{cA4 zss1v6B=_M!N+*u73!D7pCc6^Cy^K2;%YY<@G3+KgLE?*I>_m?w_os||7!w$=mJ{?n zj4v>5W!%8H1BIe^wlR`@Ai2vJa~ZE@OkgDYK;k3&K*ARoX}lo!os3b8Utyde|5nCb zjLnQsGh*!|%8iX5A#XR34r=3lC#SmybP~Oyllf|uob-Q-pe$yZ#14`Q@)JuWOl1@^ksIdakGit zY8*aIZpcoZOHo`#H)WFC`4AFawfGT_8fW*D8+E46p|BPe_J8%vKLu{G|5Nnka3i?? zB6%9!_XWAf2f15<-0eYbipv-ux}nJ(u>YfzS$ug;e%(=>jAiml6~Cx8ep0GsIOzX^4sAN5B(@?NV|q# zk1oHxowUQNcd%U^_7a5N=a$&IZr!iQ?O=O66*s0Qx+oi_i~ULGpeuw9-9cP-bSgVt zcJyRHwWp7XKu?Q6p9DH&n%WG6*kiUb)-&cax*3xh0fL|Y8OLEYuB!I&B>2eQN0l>$ z-4vd#xA8OD2Z2?%Rk#O~j|2@}}lP%wKJtZwIj6Jh@ zExYtA9q+!91CEG~I%f%#xLYU!i}9|ahGuzr z1;58o#iQCGJs%V9iezu1I($8k81&o?l`RR2gaI;Q(^z6RevSh#<>>{4%Lh^*>T9$v zx)Qh+%I+>qI>zyo<{bw(@r7RJU39)T2zzRSvA33lJvO|*Y94G&*5Ql~=YS`1 zzIO`m+o9*UyYTEJ?WNIK7QJ`nHVPttE&!_cnQENzP;uKs4DO+{;#V@n(S zk++8wUEaeVdjzysK<<+`KNP&!-*;kM7d+VelJ7^McUBB-8i{ku&(wYw#VgL*QD0BGvw}0M1LgSLPLKfH~K4`(-2Rm9nTdi{y?8=g`cvmq4QF5^Lz9k`UBek!%l6I zw5gJD<1FxEtG?;L{wuv>C(^mVD|p|9veA;~@%?%HkmH3_-0pMd^Qc^*ecI9VkXt)? z19scDlUdQLLMW^7o0(4QSgZ7t zurEhfK7Ll=*D{^<$*jVsvEFDGelyc~9b(eekfio-1p3wpbox#SU3T$59fAHr1bTA> z`i=X`lECtXp4Z%m+fpO}!cjoi=qi5PK_SNfMX#118ft7yG^B z|FXoTPXjTAr9J{238ekw%YpUmt_8w$JC(j4a|w|4lL_67S2NOnGWjO}(WG)|Ul}-E zWB;$v-YEW0fy05czl?U5`ZAF6^Mb^s9w36G(*81)gZ7t+{(sne7x<{EYw`b_naRxL zMS_Hcmlz&O!aD*I6ze465yDeN;i{BCKpx?x*w)sB1cDVAs-$>ty%|3(5-kid)n0A2 z)%!CFDv|``Vy$ic--d@JyhN%Zqtg7p>zsX($z<}t$8B#f=aZ8)d+l}h-e>K7_TFpl zwb#~EyzEQw1AYfk@K2aUkns|doQ4rx1KLS=#H9j&Ral9Pxie_{x1L}f7$;o zaI=C13W}av>S=}wdw{~vu_nBJq{8VZ;D(dSC}_9*xQun*zKRCt4e_X6d)Wk4xME>QS24k+dD zm~c)mI|H&YkQ>BkChgMWCg3z3qC}<*+{yxg+wa zNVN4)>!UVAxojI^Hlf3RA^N50s%VG(rP!+2-RSZk?etiut(I)L)6rZ{^o)dg_Ye6l z<&t(B9R8_p`?=kEzA&%BWM=KJ(Q^;2_~EWGE=b~f#`c&2R?VGD92 zi?AF1T99^(%1AKm9>Ep~hElCuvGATH%NNf3&OM9gS~@zluoJ9AY>|$UWq6fm3)&8n z8A-OH18tFJN9oK&!|P33CfY$dBhhfW))pyRMAlg}3zwUoF_eRh4zW1+>`X&r+#(lC zvDhH*r%)zBnPCMFv zZ`JUdHd!{)&Rd0QY_S(fZd71PKOt=vt5x!FjFMAGje|y8VP;5lb5|sdE=O=nG93P` znzsNDJ|)Bd$VKVvM~rUuydl;l8;?r0Slg(u4_%*>;dGC5+{xxp!5pT|^C@?z@wZ-e z_EuPi9l7bIjjN;j`q_*Fa!Go0`Pya@NfU%aD;8i~qF--IUvH&j>FZ^_-qzKgzFy|h za(UEy&LBN`w{Ga|)slr2xAAIEU%!~=hC?k6F(1_)CCt22%SMPvA>nZPddWxTQ_Qz7 z`fAuE`ugtZp?jc@?ulL+U&+%iM_=EY{h=;HUmpmgub2HAzm0CGuWyLcep8nxdbu7u z?dT3D&?DJ~eoH@JXV*&)kI+hw5HC7?iNj_UTHVwg0-NJy-;1fs7yU48Zu1z?De;V{ zQ!?#XkC5j3q-pBrOn=n<(RnweFKYs(j?T~zhdh7I*w08gjx^~dKM8{~%c^w$q@jn= z{mZ-S<{I%M;q-wS==(H7AOCImJO_T?0pD+j|F^LY@GtC@`a7%x&<0wo|MTdFKSe(* zZQvaG;b4E4+gj*{d!BEqe^+DuyRG`q3RC}C@EyESSHQ;l7re0+W3By|T$LJ@+>D2@;*J5sds$SJV8UGxr7u`)HOPVd>_ox9S zx0UBp)Dvzq$!2}BpoX^-pWaTqr=9rhcH%|*H5tpS7(CpMxuw6~0gyE$%uereQ;4tf{hQ7p_r`$&2MgF(iYl*xUVe9*m`jfEr zz0Feb*7td{iU)062ly}a*7qfJ61LhiKaH@}{s*gg>w6PFLPBqSUvKbRp0O@zFTvq| z4T+EE{e$EG5EB112?fW?dAGsgYeM4xGbH}~knk5F>E9m`P79%LoXH!qT#Na3I|_)f z!iJpZZpjC2r5aY7&`Jruq@tje)WWL(S|M&lwr`C(ywKhX^+gEet)%dc2-{zyJl#sM z+m?`9dFbL~UYcR3+8iiTD z@vkb+TAhX^&syOxE6)yA9h@c4%KC^QGPjgx=>(RnSfuDf?a!=qEPZ+c=>%UP!FK%} zdDO#I3Z{?%U92YktP+)*C3!V*BBu%;7-_f})wzapi;OMv4w=))LqeBLSme}h%xCI= z;pAD#M`T*b_o6Rro@zm42GX!>tC$*S3A!ntB`n5leD&gaqtdR%I6EYx*5+Xf-|9R~ z-dU~~6Ex$MpjZnsE;Ze25ue;ne18?+TKQ6lC+{JW4!j*A`N7K}^5P6t;^ti7^;=Eo zELJdAL63sT3Ic@Q>9WO35*l_j&E_Au66P(eb|~|K;Bd8ssYnk{uKT!Uytnu;RfQ!^ zuF>2QZ}C~$y@ajz{&y9&_)L@$pA3}i8E(N@7jwC0;SC|-rjb;1;${cd9M`Osgr#UX z+(}xdlf7kghw}Y4Pth7vg1V~K6|<(U{_Z8qm)x`DyXaSg9-7;%Xv(8;bMxNT{S~*; zuZ|cse0bP?)hLI`kk7^HTf_CM_eupMb8|7bZgU^Q9@VMH{9i6BEd7bx)(6s2J)xy% zl=S4;TpCgu5Q(u^fOWN}*R#?|R^{?g80nAd5$gGq%PN`?Xs;g}&PV7`4@p0`im-f8 z&+w7HOCG`fpd>7}TK=-leKJ?}XFCi3TE}xI@%y!-Q{j&vv_*SQlxU^+1M%kT?&{?GJzr$# zo(sv8({sP}MSdUcm-8ALW*X1)oZsfIHtF5o4V-Zq-PMymMfZ&H>d`rqyK3pF5!zXy z7duMfn@#CQbGMngvEpO4OFZl#4~1vKM~jD+%_?6H`sQGL+ng5qwnMZ_&Ct(I;@ikZ zKReOSw_?p79=UMe%lN4>^@AqeD?7vjdvdeFaR>GE#zOCS!%6>>c zCf{zRd^b_f8_|2u!2Ug==^aYs9WEoT3p5G5Ls$Bzh_>IMoOj@E_6|lk=p6{R_6{HQ zV~(imMBXv@J*1vQw=4J}Ti26a;(vCV$)B!`{Mu0x{#|oz+Q19<-NJitM)a(kdDok0 zPfhPin#M(cE0@g#P5%|%nh&*zx8^~uJuhR8T&=|?8?oqL7~J?%(^7>5%$7lqOQq8y zewB*f0F55zdC^zOwVNCLT8rmjQkYY%#6N*h#27e4>}{CWF1yEsb7bGO=mBQ};lYq> zpy=<$0>$oZFc7`nkX|Yrufif^@n`JEmh|e#A2{a)pyVU=CW7Cf@Wnt$SM25_-Nisj zSM+TXo}$8HHz(;P1LgT%CY(bbV5IvNQ1CS-oFn>MN%sYyr1KO|(tS*YHvx@um~c+6 ziWhyZlyk6(Pf_v7KuIq_K{rs+tD#avud4LsL!_Q0EP77KcNtLV?ojw~K*`r*!Z|We z?@YW@E`QEC!UC%l+^S%)g1HKM6iikSU_7TZojaUQIiKM$m>QSZj$vQs+T_|SFmhAm zW-Q1ab8mG&;pWtrsPj=@L^ZR=?^I-kw9trbRSo5`?5*B&EYIkKkBVp@u zt9Yy3CKDm?0_D1uTf*vlG2|TyTi?<5RJ`@w-AJ6oTi*#kjj+|O&k+`!b$JN{hp!3= zUk_n${Hl=nUx$P}A>kK7o_`}G99DpkE!1s@J`!#2{#L@r;(4nV&qZ=>5fLIJgz;4_ zl2MZsAxFa8voORRc?gM$waiR7t!DWu4L{-wmV9Tq93W$w*n|;_q;!_36>ODa?nrOt z0c9o==0)2vpb33-%RDAbN>)^Gp%$6v+k`JwW2GqtThlQ$hRukyt&82SK4Mf_EBn=h z*pFu9>yq={4UzM#@RybI#4cJcOU_%x2m58@JS&}K_0i2xa-K|Fl-$*9%xzE3^MuHG z#Y)brqSO9H>GxtwQ1&a-nn zxFf>Ldp12p-ur>bdzHw0mB@RQ$a|H@dn=IlRv_=KK;Bz{yjO|5S1IzFlJ}s`f_@zI zZO5U^U7bou-^zF!d&=~SwylJnJ zn`tklSwCXRdzW~bYst8}&mtt?EH~vPZQZn_9lD6c&>3l2Gn5$1kCUG>% zB(2JQyF~5_J`c7XxlhW7JYmVE#YUNxP6d2Z8QYTk%yU4f&XS zCA|+#IA;$~@_9_*A659xKuLE!P|~> z@0oDU+dxTo4^Yy12`K4`{3l_N|AhWg6V4GiMdB9#CB3mKK3&BR2TFQk&m*uKQ0U*L zQbf)Z`A@=9PyYaN-wV!{oK?tu@44z+N0Ij)bv@?VDlqb~$gRkIPr09QKZo46K6*p+ zCiV<{82NFeOvPH2`>grfGxDC?R=aCW?z7r$GHD9EK)JrG+-H3!BxR&$wd=22?i0CI z@~|#XNcc<1ea!FMlKUh&tlZZkB3Ss5$nYy-P0Lm7$$gT>mzMif0gR0l!6H_xa-Td9 zUhWHuX>H>~=&M`8U5MN#DOpj$g=#MMDf#Z2k&O~xNb=V4q``9EMePT;dPI8r>w6Ry z;n@=Xj3)P0*ZHJhl1t<@xvcP)mH*DGbS(Mr8TwnXlV_wU7GPcNt@BywB&+l`Q%)(W zj5}XO{(Fn(hV#4(=*HNI+)KZ#4d+E=4=R0l3;$)3kKol<&995NEV<8uA@Uz7$ucA7 zhqj!jl3&moU(?z|T}jp0LnW__&2?&(ZLdGHoQIy9^`Fe2b!1H&`vUH%{iv&@8;aBU2V@VvzpH@f6Y9xIbV96@6ft}HNQlT zt#fIWtf4Xo4Ksf<@Euz>Xvq41+TX-m6TcSmDJp(3kk3LTqmY2j>gQuX zzQ-2vVn-@h4L9Fci};VtZ*7R^hh;t``fuQzmw+;Vd;%!*$4x+)cNGI=o+SEKnJ3K# z%5yVKI41=t^Dil^;J+YWNv|F#&%Xzh=WBrS{BvgfvS&;wy5w02 zTlL;g#V0F*Torb!@E=v!l4I7ZFm+~J-&W}fpX8FPBz>!0Nc{-Tx_(9=I6N#QoCRTU zd~ry8ZAf@(NVq!W`PK$-QA)R=YBxvT=CiJlDL|W%O6T3WMqOplj^15Sg`a|j(NQb< zi_*5MXPQsELf`cwli3J*zh8aL)#2x1s@(He-rMf{ zOZqk~nVU=OtkNai$|d_YORR@j<<$(%>7j@ntRGRYhFV~X5t%$HiuzuL54wbC)> zZlpOoWTXfQwrjs?^*_n#!Fz%7ZgrH8{!^}INw%lYU(9??`XqTs`l3~Yr7xEGoY?Hj zyln=zTx4O~l8@LO3U4p^vgUEEL2Lh&6>s^eZGwh>Ch`?lSo%w24oJ8K%=^yW-(7h1 zs8MNS?p-}^+3I;$54W}gH!r#Bmz(iUD6IavwfK0IOl$F0|K3`Bay!qbv=cwPo%r;2 z;yvxeXRCO|bh%paXQqj_CMYf9XSWl7M?3Mk?Zhu=Cw_4|@yj|8FZ-t?9oaV}`Sk$i z0uzB!{v@EhCv`nU_`$b6WD1b3Xo~O;c)f5A=&a+Pz$yi|Dp;&wu7Vx~lNHnytTWSd zRw=ku!D0n-74#^WtRO&=z|J{ail+U4lqYz;a}u1)cUDOx*Y71hD11P|bTsnO$wgYm ze8l4K%OGTak}Hv1mfsqyejf%W@$ksFa``V|tNs2}g{}7TjtZv(!{? zlO@2lj^7ft>eD)p-D)4Qe^272KI9tDEn&A2(zJmp9IwLS_eJ8Z_FbjoRkZef6;4rz zoum(W7YOIBUYK|H!h2e1uOy0n zN6VJvg^eOwj1Zbny5yPg*!c_ZWb@047Vhv%IYT<&G#B5$Lks&7L@&v_P zHE&6i*}0WU*ofAL0Qj+R!NookF7K7A7OrSk3vJ}tf_C1W_bj}7#qws)3(?$g5$z;= zvCo9d!m7V!1!^PTu=J~1SoBLBwVq0N>;8+k`?z{^dYk*Eq!bJ1t^U3>`TVMSKbaTG z^{Dzdt*|xk{3Po5o)er>3v&~o0na`~!xdH2ly&Wi6XS+QX5+$rOX zjMTqJRXSFuF`Rjwpte#l)#q77GoEN~o`}xJ=-cHX>BBuFBJ;o*Oo~OP5Ij#58o4DO z%tOB1JW=WN&d;^8R_#FN7Q6=SF#Pz6J^wj;M2oKu@@mxe)pO3fsdtOOXM$5i^+&37 zZKQ_J0y#I4A~!qVw-5I37w$84edlJ4!(Rn9$Gc3M|l^TzqVZ?^??Twd^C1 ze01ijl84wigiAkJ`N$Q&)<+Hhx3-;g@prwA;pH&pM~}8s^BC*13Bl{|4JA6ZEq31B zfxeOV&(cdn-&4}*%enMTek*i%VwUR=`5nfug_Lb;8}F|h??0Y=#=&dey~KR?D*S89 z`?`bS1saY-B&IZ(IWqMgPsqU{|@y;e>0gD&(|o-9^%90FY71tX+w&E^m)q)OxU=N zol98snX+Cj@yRMa9vB1uDCrBn&V+OJ0EPYuprkAFREgiL;@1O3PkIkf=tipe;VM2E zDCxQt-l6c;`uJP$gE_6SPA=(-epu3f!Gv?30!sSg*Ff~Nxhnn+6+ZGx9jZVE5s zilqB7e3Iv+z9juBprrqj3FkZml=Qa(jr2|YGLZ|Qn==I{>C-tI&ka|2fI(jeBr9i? zf?E|VRxnpVkAlex0*vPz_TeMb9rinN7vP{%uhMs;|9#ZJlZBj(=;}C{n}hqw+h>?4?t7El9#aZYWDsukWKW>3fSzS<}qJ! z|7%*hwVWQ_uYeSN^}>0}zOMOkG2en*kE%vwh2`^+TSa$RFDw0>T#}yr?8b+MK1M#U zVgc6G-nzJzPOf_31^Q?Hw0hDL$YmAH2(&jp79%P7HjFX>QwY;vYvK>vONrbX(hPSV?{T}^ z;F&vEerV7FbE}%>bNIDR_=vgmImK5wmqug`!~V7f4@}$4CLTyQF^C5y?=2o|Tc;gZ zYw*F8Gvxi>SNkOE8(PXLhnQoA*2(#~O*W>QQ`yU_rZkea7jFGn@N&s`5N@6%{>9~j zFLJ4n8Ib=8U}(ETL{CK%Unc7=z&Uk5)tVauc1jx5AWF}Can_{?jRW#d zLu8&I_`wSA0ZNw13IdF^s=B#jN28<_mAxWaZdluQwcp3ndKV;oy0| z7vN+bkgPcOpM-HVUldTj)#OLE;xQxNLc0t!SUrG;gpc@zL0Q! zNVwUE)YiDJ*7vYZTX^>p{NJ#Xs+o{ z;{3VIOy7dDZoc|zrO@~?!KBv0YH+%Xj9p8?LoWM}MqO<>kTCwYcb?qHv{B*rAxT+U zSBF`{?^pKgBkOr`S?i)!SjJwt)mYcz@i$jJAn8fdo=qZYz;NSlTYhS-bmHY%u549K zGt}~bd*k^k>qXS}CU(<;s^eo!Q$)d_4XCp=-jvElXoRy1Cbbac-w>jlC+}3-@K9FM4$c&m@HU2B?Q#<{hxC zQP1WND*Iu4Oh{hEYVHxPe0}hFapAt^`~PWjoBOcld$^yBm0cSBuZDX*`ETZ!FXc1J zXcvDV5tP}<`?=UBrt^Mw^oTa*N~X@GIj^zZW6p61{n=s0ekm^<>L{xW!t2AtTRwod zHoi+3yFKGwCX)9A^lRhUcVqfnI7%9#ml59r)_Gf!PdL3yxII0&*4bP3f8KY~Xm`!_ z|AmG-ZZ809TV5MLQK#|=H@gHc*GTSg@uJ_5>oM+d@v}k7r4nw|_Xu9}D{{3KpR3|! zoyT) z5KjPm0oMa1p9N<8vRo6+nGF=WDL{G8kwCGN;29%5(l+v`GxK*=DY#X^Vg+**^eC9D zpr&9Q>G0E8rQlWtixtdO(4%0of&k+=hdp<}Vu!t3uVm~&^>_96&<~2W=mzHZRkq!> zJ-DlV!nT9Zdiw_ZCcDeF!~PVy!W#Qq_P2pgICeOmLRVPlIO?c(xNPeqHlQio5n;Nx zEq1PVZa~b?v`j~H`CvTtF7iRHYNul*J}BH*!o*qgn$P(y@pOiCyo9aq%1K@lw)(kkir)IJ2paL$cXWe_x4!Fb>Upal+NZ)+zh>zja*tW zAZg&DEKh{(ROnYSA!$R>ZR4FR)(sV=H#WQ`RdT4MqY{{#EN|NoJTICy zthT+4!iQ7Nw?S^bYdCqowJmF_^X4s^clYw|EW0P@Md!|+x4O;ad%`}|0q!=EwB1mY zfs56uEEh9abvN7dX2qNSgO$lDy%oW2-qPydH>j|sXAme?N9|yG$pFSB=}ETFpl$9W zxNg<8^|Iezv`^SAs@R_VxmM6T>uU6W+2i(#VKHFvH`FYOuwADUjV zN^iJ&Y6s~Bry)B3;7jxtZRtUtQR$s8G@p7mqsKhgJ-x~fnP_q)EB(w{ICv8Ny|7c)QjM7Avc=rF3m4D5^X_@e7n+fw{M+** zGrcM^TKjObst+c*GEqXWvRFkKpgld!aGtZ;h|DQxKdo!ifa%Za+PhRtu%2cJc)6L6 zYB}7&^+vEmSBmBdxZm{rI5pREKJ|B52M1Pp&gUGOaB$G@jPv=m83zZ)cmkUOQx3ZO zYyPOgx<4jf3yA!h(%<7BI5@*UINlRTt5#GC{LXS4W zm9^Dda=<;M#~9&!Bfe(K3T)wb^!bHv6JgTwN;)~Ulf3eLer=9dp4n78*(>xvy0H47 z&EHtD_WJU)(tt8lL`r!dO5yRgPrQyAlWvhYpc&kG}cd$p*Fa?M_rqsLS%(;Q_- z3afob3!{Cn71sJ{3uAqiy2FTf6ujxv3L*(>6}iya3-|jRg>k-x(C0(%)uJo%w1~3) zYxeol)^zga>zym^g03HQ{h^Ckv&ZLL zr548f;tTiqA`0EU3DnmH>TP4~UZZWeHFwz*aIxUr;1=udih;J+vS@Hq!DZL(*^;BV zE0$}qW!d0<32rBJsd_}kX+6FyRqs^6I{~9AcIutWa%gkXR&u&`^-TqCq%I}>rNGnA zy8S20BmKu`X@LrO`VjZS@O1`vXKoF?_T?^zSC#OpKkqP1-j#Q7@(!o<*oueYZKdw4 zIHO0DmB6d@@ajt9hZFBCtRfA~Hxc};;D18hY@%+`3lI3x3OoC{gYN|)eD1vTiGT(u_E5)0>)Lu+3aPTp*aiBPSw_InQpgL^tJacJEQli$kck4 zouZ9Q*E*M#LL>2Kpqr#eR_xWgl%-G)ll15^H#DhwTv-$}Db$5~R99alzuh(2Ua2GZ zzHIMc__&;V1w4%79=;~Qm%OH{FB4w&qg|%bF30h1(|ETl*X;LQwImGRO_VuBxZ_+j%f|t4Qa-Z-AUP?JW81-=3FygCtf1%0O z<16;rlFIV+E|kqxwgj4LT`L<3%@T=+ZaBQu?7hn3Z9OZ{N|Z&~A}gY`=(6F^Mk2lLVYqJ~i_JV5qifF=Gg$KG!zK<;9`!MTvi55wF zqHmy1(x{VE+TxAS&4(@zy4|$r*J$tbCk9>rf*N0+f*9XCO*g_N&~1WlIc+3AsQo+O z$@}oKUfP|_ZPZshxH^TCHkxYF%HqJKfs^)`!aGf(O?L*DtZ++=zM&IsErxoqtF|3f z51mz;rY#%w@U-18byEL^!+&C!Sr3P#E{>RX1@!|afS>3;m34{l7T;6h`;G8DMfgsi zl)CTDElc5hh2F^+i}K;S^eszi|AU~J#P^X3-v{zLa^EEHVBm4a->I}=t6v^Y8@Br8 zd9>q9)i37Jj#o-MrkzI6PIofqNjuJIY{xaU;~4srw|uLD+p+4Ot#;g(cGr(~w_f$r z%d{AyJ?7D0-$j2deX6u$8+3M~9luFCj%;kl3kqNH%`4O!+i@Or`OwAEjyutg`>Vch zJnvRdTRg$I6=jR6kTGvMxOi|f=4~*>y!f&ha5KQkm^To9&d}nGF|RZHjM}$%i}Y3I zSQianrSFV_pPifd+12Ey@No!fJqVlvKYzjfG`#$>{9MO-mGEBe^Roti#`vCqUdB(x z>}LF|Y~*JR{EVSrXpf&Y@H5626ONzo?S$};ej!SWGWr1FTMoD#20x?9V&Q>>8xB8n zpmXnq$F*-6bi&U~#@r#g!fWs|9)3oJ<)^ey|E2bMseLy4-kaK|%-e=qa~;}>j9b?C zF7u2T;AGqqoXA8&7`LPku;#M^>BFYdf8GdgD|9l)mHEPh^q+2Irc~Oe%qLQV=MNnj zw-~cz4q?m}E{$6)<_jGixANh;j8!tPlW|M>S8LolNxzWFxunw1KFqyY&A&9DZj4(^ zGQ~JGf4vg<=_=%>TKY(l*D9HFu3`?gi?L`oecpJ+pc@&3(rl)@*1up6b0#-oy<#48 zdGv`3Xmj_gv1=1!m&j|5g1yYuq8Pg@xo#ihWGCPI%%A_l{JBY9+l#yw)hMsY{5%J` zPK>Qj@@}#FYPZBO#=04MhS*{&m{*lO1HLnDa~^5AkxgXWicxbdnVX9|zJWe?3Vp4} zYa5`Kw)_;b#Yx(5t*)1ua{@=%6y{>W(~Z#N6Q9r6wVdD4`zCvr0S5s&SL3dEpES%o z{jA-8vRuu@XwP!iuhphcKAYt~@%%Xd2g5vpmBM@OJ;>0d%=task8>|UZr;YYy^DK+ zk|SP&zX#y2v!K@JDv0%+MBd#EUq#L>g^x$!>-*Hh>&zLdkm<1CGvdcXp9wvhJLVh_ zW#i!2c=+@~RS!>55Ae9`O61`_$itg-y<#nNPY7S3bFSIzbFGQ;J*l}2x;vrEgN`;} zgkOVB5R0A#%p$a=49T@1Phpj!_e^=X8E2;C-wmj`^uc&E6U{afVQiuz*p!C_il#Zu-4 zB1>n$Ba!`cYWHv13qPl6dRYc|nak`aZZAB22%bKz#g**^e;C{cwfnZn_aU;jq07>p zWe*v=wUyP#v&2`z)2I0zTQk-B3*Zo7zG~ZNuG9Ue2ig3mdLm~?+s>kG1Es${^J13& z)YIeq+4L=M(x#VE52tD8O?43oA0vJ9x$|fvPjFjx@pIbkR--Pesf*}V>tZ`~@p!Yk zsHQHW8|&hJc=yAwbzy!7R$VNF?oOjFYN!joo5s4Rr7n;GRbBiXI%(Id`2MWAkhylW zs*8MNXsL^N@V10+DxbQjrj1Hn%mXj_6se19+Ui4m2T~W+;H|nCY>P4KViPj=Y3f4S z`7d-^*^i*9p`Y1wd6e&Ae#g{I@je6`40Ky{@uD4mJN&0DnJ>7X8G+0!nc%j4X$<}5$IzHdZTs&!hSIlO z8bklSW9XUdCeZ&(^zXozd}CcOo;LOQm*z8H>wM-qC;WE#PqUU_=&xBjsB@WX2|Mh! zwK(6c59c*pxbH^RD`&87Ii2;(X{=*T<=nF=_)Ta!PvPU~FZ)wcJbsa@CNTFLl%C;V z2pquxE0ow=mbH?qL)d%)(Z7+J8(x6WoOOIYLZ z9n%sj64vbYWoij!&lN@(;pYlp^UWlD7&+mGwk~A@kby@a1CK<0T5U_H7>O*!b3AW1 z!U=138R6#&9Y*-M!reyr5z@JzG#A<;Dn=k{$TM%(yt?H{@*IiW@j7$QzaUQxU`@m* z7waRWPr7cQrCfWA@N8~fRqs(pJsCjLRJTsEDlJ&@T1DHcgx_8=~ zWh0Pzjxiq|f&8%!|w>bLkD_ldZeUme#ys z=;-rF6CG386S4irRv?f{n@G{!-sIv}$M$i1DwEgOP3u#-l=Ddz#j(&on9H67JtPihmr?>;Ae?@9ymrQ+56M>Z1R!#kr@E_X=(O;c4iM ztKE7xFFN7sRG{on+4zv{ycSb@y}Q4|uX$u0&OGa5+cw$TAbU{E{hYanIirI6Rc?{n zUdhx;w{cr%>ny&?^{7$-m!DU(i-IMnNt&^8N$NP*e-h05-+501# zMp^8X#o?c7kMXYfR<{2w>guc>?ag&g^4qjXZ$B-{yF4?(e^->#Ut4>dcUraQ$7d*a zCFPE6#EveYI48*=A2m6-OJX`T2+xbJIv2jVZ`y*2v z{vnK6u_;<$6p)RjE4AYL$7;nt94qVIvR?IjP_g(tN2KRiW zbIL+bG95jMW6hhazef5hkY8op3O*R?uhu$?r6X)we>H72taU4EJt}nFsvrGf8hwkk zZq*lEhOBSx=6ipQZ@+2%buYT9sK#}xEYW#DHw1l4s-Xik*UT5dtCPst(YEM{IP48p z5f&Y`;6!gU7#+6gib}zG!42Ylrl1eJCkV%PUg0-%*rK}@9k%GQJD2rGTo%77p`0O&@TEK7hW`8{qqo zvPV_lLAyM$+7sB?iT;AVBU_L3K1jNvGkZ5eJMh9sqx=>8USf}|z7E<4ZBf-Dfs^df z)z<>8a!Mao7wxR9U#$hCoQEl|_5BO9`VNVcu=uMn-i5wl>Cg=SGWvq1HT)#>eo3rB zCb5Q}h>j=m(i%ScxSh14*4OY8O}muFHT+1mh9BiWF&CLP58DZDsTbkk@Q+w(KiMn#>knR@>apHm^ZOjX#a`_6t{nf_ zxu(rry)k}t_a5)(GR8$2bIdWNyBFIK`nT@h`T=gg=)_Yv3)wB<$y#6!{oQfBmv^V$ z(;IC|@YZj4_)}~>yeFQH@YjzP+XH0vUFNtVzjgWRR?)|rUC&9m-gnJR*2p`?NTmF7!qlf-DpRn+B6R@&yluvj$ zN>B72=i3_%zpl}vs>kTj)hUea_1ML2wc8F@W!+()o$l7P%Z^7gUnum9iJ~9K;mmFK z<&$#UkvTb1{++hSjZ((RcK1fk^4=K@JOz%vY3I_RntvnVYQoHYcINYYu-%H&{U?|Q zPvX5e&wC?%)Xv4cn~dX<|KYgS=g4cBBOmYQN#&V=Z0f)9!hQe3xHFsaXBOkot&B&v zFkha@eA#LF?|r0Umz}XHlDH^nqM?o9nOMdu&h;yiej>ysuq4PP5Wf75I$y>*QY?3K zo4~G(Y4E?jc>@efe5!P#V)9=^P?^o)UollcZ zpxF7?7_aP%SJs*dx>;jAzh96|px8(Bg|445u3_WHoVjV7G1ovWJD*+9h1vu<7?;F8 z;$m$A#Xe$c3!6YkkWHXzAJNPvuxnyVo4^rH{|Ua?)>q(yzNiC*aSu>o0AFPCxUO^Ch!&V z(|seMZ_6ey7dy60)=tDG(8Ib_uuY(iIahO=Ko>TFq6-(BKwF4SpkC2O*#!1cHi5$! z!&}+}CbVG_sQTWpHi2ES3G9kZU{~z<%=u7XwN9SJd_!yk`y;4BG~O`xMyo4|(* zn?M(3bGBj=sDE{vz^NuL#r{m@s>iWK+r#hrT}|tsvOaW3$?;+%*uHI=*`DvB9xlx% zOgq8$Z308)6WC@-ozzckvds+Ep@+8#{J+ijj@ni zCeY+(c$>g4&reHMUBEckwvFXKn@wO7KL^u4H@6A=^8DP!nD&3uX0?f*VpH`Xy1gcw z)i2M_e}wk=_pu2K8MngOvNM0M?58e`TP^nxbadQmZWEZ;j!j@!^f@9=bj2o6WVJ-T zcT=XY^nI6X0z+*A6EE2Wvet9SCh(F?;3b>DOE!W37j2I(*#v%rw#S!j0{^~t^_Oe{ z|Gswhmuv$6A?@nLHn5a+D-ZYM+>5cL*v2~1E^ccr>VMPz9r4p~C%O&UyOYM+Of>6g zvR67c2xr(+fRjBvVoR}!byHbyll?og@4>?DWF1`g@8n?D#hMdP^nNjh4-MJ#lhfVp zn+$zaO^$aea3E{jE3i*_n)?}ec_Vi!em|12qgy97A!-d}J$gLZ^M7T*0pC>x?8m_7 zV>4@e`Ro^pw?&kdvsUv2Yc=+sLugMJhALkn0(D~Mx#X0NY+az?|O&8))+uf#t?ysdLZXVzq- z9K)|EHTa?l@1SEH8S|X3tP^#C2QHxSKr0jdk?hYC{|q16Y!y|u9;DM9{|w#2`ezUy zDT7$^l6~X(*cPxJ+-!e(%3W^5rXU5I0>BD(j*>ep!#-QPP?{ z{q!(YOd=>_Z%&SQrVY}1sAJ<~vJnZ!>|unkkJ z*g9e7GzlB0jo3HEDx0Rk*aO61(-dsaw2XEl`xwO!jqDkZ=T77Po?*|FM13W7*q+JK zXI*?9#j3Vz`^!T)jtAG zvBy>aJ5cif)$`Z*KeNZ~{0;V0r$$flZ;H@LE3xUTziz6ZJ;vBPMj19x^d;U?qhtK_ z17iIruw(M7wh-J7lBs`b1M8>@*^3ow(UzBgC))E+Qd&zuuqYsi?T5?57 z<6ql$V9oiy1Un#Ew_Q9z_b;ZRz!>uqVfcd5=U z-v3VXIy=su1#_((e_hzf)nWf8HgJ~h-WT?sI~VW2)q5WQ@^4~Wypz6GY|qZwd+eO7 z?9T$&O%2ATFW3fcnqk*B)UZSQcFUk5Ct?F=^Veg$k!^Q- zPYlD)P(Nk&R$}h?6&pCq#&4@jlOyqz{cDpAXOdKQ0n z=LhZ`(o_!=W)-+_Fm>>#lU&o^Rep-KGWgLreUN>TJy<6`itN+zwUhvQWsC9 z4Q+Q{!%pUbCzuCrWDJ*m4YHr%B=t2<_CK(nLh2HmYqhT-+8dR?d{6CZNM4^=nUaqE zYC5>}wo0q5Y4Ed;$(3{uS#oiOQKmY1B_MiMO`7Vz4 z`_1w%R_e%|HssHi=S?@1^gkfy#F+bKivKH;?>be-=9=4I((U|Uw^o`Qqm>Sg*GdN_ z;A6F$R(gDp?oaInWQ^^btd;ifua#bzqLp4XSSuYhTr0h1q*gjQT`Nr=tCf!NXrdRofLyIKZGl#Ln*Cvqzw+nUVT-lWpTFXX9llH} zeE>LQl~x*G<%u19k5+ns@X&iHPXXToHriK%KZJew2!V{LBZ0SY{~P!7-0yP#=2cJZ zAGts0?zPJkdp-BJxr?|raPQ{6l5{h<^SI^xlXcI9)N#o}U+3;~REtZ!yicdsd-dsb z+~v6NmuPPAt_j40JI4K&ILC!!T^tv#0yi-jp9H?AqsBm1Ht$kz-N&@xHp3F6Twd=eQ>VvLFrEie=~Ua zwVro-l6yVx^(6PvS&j?u-|o23AO4-|=Pu18E{{8txIFH|a~&6skRl632ysy5~Oz=|%q`It|&Y#rTwvwj?F}h9yJO*&|R|$&JsI(kpbw*{xl* znO6ktXR{j`W)5AlIK5Lt!^ojaPN#Fu!^nN#zvAZMOX}12>+#VI*K2pxFy0I^(ixI| z#V7W&J2;o96KQv{b(;L=>$S8&gohIU0p~tVU$P?oOoTRaE_v=FuRmX~pUoo=d9MER zhMB(rH*CoV>1W92%y)Vl`J5%68#1-K_L7fPUZKzZtYKyx`C!*QnR29^3;QoAK2q|8u^mWo{z~lHYnfaz1qyz^O2eF zGL>)DUmIrrS5Ur_30wKDbhMtYc9wiQoB8gS_g&JnS-w$0@5}ix34tE|0NvF668hq$^u;GR+iM=zDu?H88NmoKF+&!;c%%d_LTm(ZWz2^_$Ere8!;0^cag8(|eipcD0?};WtRDnzR_dw(;&u8N>1! z!-W3^{}{^})7uZ9585xx=Uv0`@9^(Q>reIz>qut_<6pU?#~7H;*w^1mkMYk+@9>|Y zKLUMg=^Z2fq)LzR(nyc-GM}-sF+Il2#`MlW|55Yw>VoR`@TbIoZl-sSzDC``PD_r$0rW-hgh;J22B9gIw^VYqR{r(1UEAkm1*CoxE$K#`*i% zbngP)<~`-o181T+cREfF9OoO}}S!b*Kf_WO}nr;4-O)D)vqL;2kXJN^T#ZFIUoy%Fd0okv3 z-K%4@l67N?*X;t9jD7Gb(n0Tk`~fXc{KL%3x(D2qM%q3taK1dEG{D@f4tNMCa_7e- z$no5Vz}d)0aH50Jk>_nn))yU&&hN%}%3{zfncql{G!9c{d0zH;2d9?|U2uBIq!pZA zGWiClS3h8i|Ky-)M!lWBZn{x#A5NTV)Z3xx8BO&zi+a1AdYenVdRGGoaZR*!+ThDWbtHOiS2)u zC)?pk1w45go;>>v@#J~l|3!H6GCbJ@Pu_qh)ru#7ws;a#S$xD9o+tK*|5=`V3Qs-Mq%-X#GCWU8T>rB? znP>}~nQRNxO}7P(+zd}5j@FPJSnnxl1Mv=4$l*N)c-6`egaQ? z@Z@Lkq#T}X|Au%{0Z*QWC(pu@=i$kViYNOmp7f-hbPCUtl9;cBC(>_KMXkNQ!+q^B zcykiooPjqV!JALNA>MorZyMl@-5xmQvIkB^+s$w23VYxc=i2LwkHm-Pi#_h^DdVY!4itZV#Ne8NSSdFSjectb{K^;Y(hZW_&4P zom6Z>a`6FS_>5q!V*|dBJ^}9N6mdRxonD!{?*6eSt|-2#9vzIx2)XNiIQBDp)WE1E zKS)2JX`==LQBIG_acMWsjnLBipqoGrPq?FpMIN5J22O!{j#d3Hq%T;`D)4k zEIsjZ&yTObU#yokwy#|7zhxYZOentT1aqIzGXEQS{n@GAH4xnl$CD+T>ykd&!fiw! z^W8whHD5LVdVAtz_vOubmk94V{9V7C=&yS?$=}gA?~BZ>KWA>8gRc{r``%&9eaEVK zRFazaCaHOElJ{tNH@`XOP4XVE=~=(pBBP7J%LaqZ(2b1+%^IDOr}_|IhB=0AY{s;$h+52-Z<+dgxR;YgR;{R?YM zA6!3Yo#9E=8Hx>GTRpv1+F0LTuDHX0K$EqGB<~^oarM!XyyxzlkUcX*X$A1 zPXg=SNc11_Ci#Da&n^5}d(Yr=>n-uQWw(3Zf4-algO|JeKYXKy|MYd=raW`~XYCn* zj`{*~!cY08On+cwee!K3>*5bA$(w1cdn9@DjCGGB?|NPQfKk?xp8mrR_hKE_Eo+0` z6UaH(+IjJx4Bs#JAFt@`Kl$tx{xoz?Ut2qCr>*b(bHmIW^4n1|yM=GU40L#`Rj`gb zfwjN!*wc+;JtK>CjZEz6GW^H;y|K(VOPxAz$g<&Y#ZH_9nh0o}JmX>w)9p`{v(|=Z z_=m)M0z;{P@#Xv$wr+P}`)K1X;TAvJ>#;+8lKU#B9>Pb)CVXVX zK`-ZBiEoRM*h|VeJd-$|sT7}weZlotxFy&+=HoXc4O|{Fsn|{Wp{vGbtA7w~19nc; z_!h(t02@gckmvB@6$#BzY#A1d53i|FKCxTN#{WkQa1izwvBNcgbiD45jK`PX3z`05 zyR-a#@%?9q-@CbAgU^5A9p(B7DEc7WG|hQQdZT#V6Q% zy73G0q%E=RJJ^cg+Fc>NTAW-{a3h_X++J&*4k)_wdN@ zBX8?e@hg0IoGWMNd5$y)JL&NeaF_#p!><1SACDHvH9M{ z*Q7jmEB-1o@m<*uKb2|ttUL+g@J>IA7In! z0{0rc-UH9@JH)$0lywTC8-~ApIb-h({_WE!S3S?A(T3unN!bUV=S2CO{9X)yBY};=2RBX2RF`oQ-xHXQO?-<_+JcYg~K_Nfoo8D}&Al-DB|kr||nedvAko zY|$IOF-0!l_wC)y=b$?V-Ijv4@g1Mwy9d7K!S@o*2UviA%sjrW?SI>|Wj(gL(zkbM zcxTJUe|vSytcE{qS^veZE!|!#`th$myZc~0_2G_4q>j6kb%kFaQ6DDGRxzJ+q&{v2 zmu=8>uUJhTOS`^@Z)CCXJHlR3V&kk0;^i!|#ko;Fsn;Fw-v&H9K=;>E2lZVvf0qp| zzxxTd-!X-HnBnnXGc(iw8=KpkGBXPs%LMPpJ2L#w;oobNW1OG1DtG$sc`xT+_q%|JJ}Z&AMEjE!}T5&sqpsS*Sza1 zThq*9)sqmYkn_$FZ*x4xmSx4 zee0;J_ln*D*Q@L$aJ>t=`AUkq;=`$Tg}d-w-(y9;^KG!ZNmmEgtKt>P=54;81Yd8~?A_4bVvnpyA+1P$yK8^9WgOqi zVmr?o;oVyf^PMG7j!eGi68xUdlz1(&%w7BTmc;Kz`QBped|r$3rZY~RxFXY^jeozt z!T&cKRv)~Lxy0xDW_Ww;zulW!crE;jt+srA)@t3oS6OyUT1@qo{C=utf>(UBr5e83 zV!dhD+=>m^9_A%B;wRVK=-tPdxCgrfzQgJ$a9QMcMt6Dtn>oO0Tkq;Mw4H;~ul4ZDw|ztdQUe<<|(YH#)K2Y&_|1sVGv=l27o-A8j( z%Qqb9cKTCmOncvX;P+v-u#Y_XuKf@3{C)!$hrWsV19xP)?tf7DN|{qApV(=+J@m)6 z?%q`JAKc*aGp<*!;8}dYR!1ef4BvAnSTE03^pcjlzuUi)^LZl&|4ffw6{Y{t!r zjGq%2N5?asj$^FN@-w~~w(G6g+?>On;FEtMf3Gsvv}|r_hL2aEgm+vuzR|y{ zf8c~h|E@dH%U<&D`sMt)ezEemv^Sl0XQTbuX@~d{zApmbu6k)h$qfgjjeV@z*rz1} zN(1GcOPkxIbw8iYxx|xGOdbBzbW?{vW1OkO&u|Cn@Fzx~&x$~w6@fl0qQf%yFOg@? z8uCnMuO-{ax0PeaH3NMj--w@KQ%~H@dvsPrp#JsfT(H?1wmj1X(bo@@O zat}KGDs=pH>(-5}LdRdXu4L?+qT{zm1db<}>DT?xR%xU;C?fD#zka24+=t+&wCg%} zdWdkQlBGl!pTV6;9Z0j%WabKtR5vknFfH0q#!WoDp$zwm2d zsV7Kx|Ao>Yd{UmwI!SVACi?r&p*f^v)6e?F!QVLK*W}V|s_*c?tHJ|l4W#f+wQKiU~MbDa~Npc9>-6P=*5!~M~3si)73 z{-{S7{YvZKLH%SW`avi9LF#B0b#yy*bS%>3^?)d|j*bmA>uA6*vyS#>1nO5h1Jb^8 z)w)EZ{V?tLlk%Q?13gRUvo;a@4Fvi{!Sg8Ebk9aUcZA0Vy~W>&@OLu&y&e9}Qv96@ zfAbW7r^DZy6@M4O-~1r{J^+8c@b_W(y9NIKq~rXRHCbcs)PcUG-Um;A22ac3>2`Qp zA?==Z8l&C2Or9=EGkLl_&E#njJbhW(y}72*6Mg1=cGgDO?_hlU1ES667eyP-OZ%^< z?P+Ukj6PmlTWj?3_4EtJ`R*IPfspUb_y*(~GrocD)JONmZy-?aqTXDz=k6C*KSFQS z&-QQoPefBUE>$;i)J+#vH*d@}*J6{Xm!3iO@_Xv#Pt?nwsh1xwaCZPON(Qq&yLxCDPx=G;8bU z`bEH7*2{^r`tvGy6x^Rw(bos}Csm9K2EE10MXUiEZN_GnZP7Jm*|yL0pRBmmT<3mq zmbuRT#w{jqy|bIvxd*rcX9l?fb;DeNBO_daqoZ9N?oUGdeHl|l&T6mUufNU}IDUgG za3aeUI5p7~I62uBsNa5#$;&@8|CsBdjmN7wOLx_`v;J?4wQl1*o7%S7ziVq%|4x5; zoc^-0ZHM$zM%$LY$!OaN@IRrkZJ#S=KNt5Qa2Hh{La*whu0-Yq*d!QbO;>f1LtRW! zbum@d#bi|%)0*nyd(_2!)I||>Q9@lj@Kx&KTBSE{qhI$@4-ZohTd0ShP!B%pVZa!( z9v-G1o~9mH`!VN^W<9vEGmON3cB9QRW^$}{*~x8*V{!@|H=p=<;+Tz2h1;oczW^7d zaQ~rjzXsPy;eM-d{}0?{3iqnQy$&u>;r1%rK5#u0?!Of79dNxB?vD!h-{ATx++l_L z3%IKk?u5d90B)ed{Z--4fg7rDpDEnmz+J6ywj}d?ok?RhUZZeP3fBqTwF-Bc!X<+H zmcsQ=xZdD06|S$sT?KA}!VOfoq2O{9?rMd*2HZ4-yH?@81@0z=%T%}t;BHm89EF<( z?ly(HN#Sk<_icr{P2s)`Zob0JSGYUD-KlU(6>bH%r3!bq!hILq3WfWD!mR;!x5BMc zxF3T1uEK3pxQD>~K;eF@aQ_C5JwHZUD^s|iCXLxyZaXj;+w6QjGJ07bt@DGIJGyL& z?R8nc9y56|w#Z`BeZ>Xs>>>7r)&F$X7<5q4^*m>-Tgn=ytX-aC&2JBDmmkX-X3*ND ztXIl<=GKQE7<(RhCwJY3v4Qd{OFwfJ-}EAR7fsUw^I3zd>xX?4a~v6e>!3RT3lc06gvvQvA z;cf4XX^XF4HJ=?N|I%XpNY1uGZ@~J|cytNl&?#i0TgXJmkbzF?b>mDKsh5T<`+;+_ zqM%{F+kG*dUt}+}^bEK3a@|~S?>gD7+oEoDxF<{dLg%3TClqrY%-XST*FmKKkL!=at*V4?U8KK3C6EoXDgLBi_eQg<82Ym z;x~lGuX8nv-w_&bi)t3XCp3OtOtbj<(0E&1vv{|hPaE`JYu9yd7N3g_VxH(9Tw3K~ z(LZ$ItOe0gV0(B7yTirkDHu}&xrWW54VsDUtIqZ>yJnKVwLPnq%145n?^22W(m3O# zu<48!(S4?J&dUerU)bAKU9Cq~%NbIf;Za>b03Y7N-2PLeBmIZ$$ljFQ;`N35rcn2j zX&>w_%rf^EwyleD9gw|KjG6!ChEGw34FY;U_D}7Ua~w3YZ>uOtOqX$V=a$6ufewW$ zSGZ@vxfO1g!W{$`qi{zQ?jvyV3TIC;)9aFyJ}^Py`YGH9aNQIxOW|$?*Gu8@6mB)R zWQ8kHxGmuND_pt4Jqs>H;dUw9L2!c=?uf#D1a7#(*}JLobxR*OQsMe3+z4>#3YVpD zH-j6iaCr*18k|SrN)&DjxN!X!@s(jtk2hLQu zehN1N+-!x*Qn;JJ-Jx)K3bz_uuELcl+!k;P6s}z1o&~p9;dUw9L2%0y?uf#D1a6hW z*?Xw+^++FhkHYm+xDnv)Rk$pLyBS=8!sYcy50p4cjrO*>NBYh@8B3Qimb#6xbVIYT zG&P-b)=O_tV`&m?*uVO=bk+i*ckpcRczP{yGLDupo{9~SjGwS^D*%@tM)h;#-X2ePf%&FCRMI zzXThprF|#(f5~_|1wZY>ft?tK#aE7;$2M5`D=KD87XL-_qO$yXmwEi-(HUq3SNfhr zKlwiT$wfUf{hhIyO14FK&oVwg*_$2d-E>S{C0cSy$kG#{MP*?cGuqZHYdMr z{zEo>ugz}Tn`iI7cfGyG-g!}W{B`x-__#wmP^s%1W3||wd6zl-!)@`^@uZ&*4m+rw z%ZEnz2ifF&!rq)Mr5k5R4YGAI!mNL9T+EqKOZz%Gt4r2$dT&exhYYfF@wHl@t#;Z_ z5`k?}JoC=Z%s($fEG+)Ew%)+?g9>4hFT8j@ZcmE8(_~SZ>&A>+P zq5NKrtx{TH7hkM9(_ap>Y*L;qd;|HZ0K< zIrztO>oLI2WjWZ~L_?Q-c^9Afa*M%+V*{{;_Isi{%71EmwEyHHEfC8(VVq~2|3++Q z9*4)}@OU@(``mwlzpgc1e77h&mqhGX>_K)zv@^pFDU)|CH|&aPu`7!8C1GFVz`ka^ zvQv5oJEPaJGlCx#KZovj*p|J6ZQ1k6hhD8M*04AFb-`QMdc|`-mudI(33TV68;$)_ zI`&VNozkb!9fR%-&eRnX&cYVnXN{_?m-P~_r=W`49 zcd`3<9{aB+vH$8;@P^M-;PU+jKK~XzkGDrujJ7A0N!vPxjn!!EztXY)>W&>#SL~R6 zh256KzYDGWeuwAI!QWX0I(B4E->2Ya7rX-RU-vof&a!*SuX~XOzI#QtqAKjhx`ESu z|Bek)H*Ar<4{aHBJqDYo9+c~LXuDxoa~rgR`z<{A9G<+-*}vFwRlH&AS@tV%=M?UJ zaM-4mZ3p)bxYwYI;2o~ACzibsguAoAfi0Vj{F5s33OECqJm=#>%}!Z%(UvsI;nB3R zdD#4Eq&J3VwVGG9Xp6h}dhvVfK99F2@Ut3^_lq+T{?CqJOQPhXe06SbF5idjJIgTo z;y%78EvhKKZtm} zsMZ%-6zj_bp9OwF(Vu(^i+cL5Dys1fE{gHFz$bveuJDh(v4uT+-zwT?NixnzlCx(95kCF0$8Y$kLf$?7a)y5sYwL23N{xhH%*6gI*6;t80RA&nO4W#xvpgQSV`B-9nCxU zB^?`Qre?@F%e1S-sVP2u4so9Hk!kfk#+Nfy`5tB@u28ko<;6?!`j;!WA5$z6X$ocek;7H10DiOU42%l z78z(Aa*q*4zqyv*jq%8E2EDabBI%LFVd_HY>qt8|y*lWE)2kz`;PmRqH#oie0jzlq zQfpqOYRxM;Xw7Rsb#{|o9|DU}t zfr{$b+TEv5&p-o$;(*HFKsPEli;|=P2L@**HAY2@0$T20T~)iLUHeq++Pn6y zI*ip%h`R(lE4VGzK#Y~5FmF8ekm!f~zF!NDfs-we@%>wI(i%2czOcb+4;w81{~h}) zD;@tSoCHW3YP!M(t0!!*`oKQz71*bRA&%oD-i{N;!n|tvAL9ga&3`LSrUEB3fRow4 z$$Z$QE&A`lOHBrFvI00U0w-&MlN=jPcG_{`3Od2J@`rR{x9$8leLJ53C#As27r;pw zaB}v)2`3f6$=ATiCE(--;N*%8ClBp7X#zTN_+y+ve*JHy6KB}zxxr4)19o~XV5bLr z!ar;C6bb&xdS>Kb&nY#&u+wV~J3W8c=>@<}uPf~I__qVQK&U{mLvS7O2)vW1+NuZN7SGGiI|yybXS1iu6O&dQ9H!Og*Mi~@WM z*l#@_LJeAr&F25ze6;d;NQET!2z!y|K)Zg!AB_7Qe=zQM`~g3Q{UU!51iQ^wxZS1< zwwVgf3pp3oxZ(Q|g$AyJBFKQp*?e6jE$LgY%#hY|ofAO@oFo}guM`#a*7eys=Y~W6 z>1ng~%!E%Z&&RL8=JNlp-!-DA#pd$&eg718Z#C`vK`+(cbgK`1K!dNp?A zA$MNsYx09{kqA5~;J?Yn!z;nF3*6Tyc<`0r?IVy+4c?sZ6QaO-Mt1jSRnC9!7u2dB zb1>Eje6QzV%v+Kkd;`30EqL4-@TJyqvGVQwznyRAa#Ty2ou_*t?T_@X+-{G^O%1;M zU&?Ql&|lQTj@M7{Q`6m}!7Av8)X*I{K&RvgU6T`ZP#Wl_oT0OF@ze*oHq{3S?)o5! zi#|wn)CWnG`XHGUae7MSK8xhIm;QKq|I9C*46Qg$3iWZX{%ZI*R~rQs5T24^({g6OPI%>B$Zm*l7?eHro*{|VJgxGWwuNo9wDfr`~;<1 zi|@$GJ4wy$5BV8%3H%3=R6j-Z^ywr~>Yujp^ntIutC}RVN4Yq>K=pKJg{3U-a2@wI z=nHt!)3+0cNd}B|aYTU*FsNIn^^VYSonqJvFe!kETpU>t1sL>47wH|Ta{x@D4Tk@p zfd6eg+n~&C8hf~}Eo?x3!{VrdDIrk>|0nq-|1Mt}&n@WRHjTYp1JcQJvxX3_L0#pG z2N#rw3@#{s8eTCC&v`ro^k%Se(Ul9XI>=n{xaijLG*x<|&g2@Ih4?JQol~U`4Bp275VO7_}ab`vr7ib6O4WjJF8gTAd*3u4>X7 zO(1Eci$u!*59j|o<3D6{YHy6a1o}3ff2&ja@GueqhcX&j|69s|?v0l-L2b>;%URm^ zx8;b?#c>$iAFi9=-U{uI>Ly4D?fA8xNWojD#iPVSpmSKZvIR^UgtSbf` zIg@iF^!C?lJ0l$XBaDRm^3;*HUf0z+8 zvCH;967NXFMGB5OPAj0}M$q>SQ{v5O*+Pe)GxAnObNYRrAwoW_#J*dF{kIzXa0l$i zaSmd>lc}-~HF%MJt{=WLa-Ft!5dMebzZ3rNP0M3iFMUae*ZWlzr0rtKslhdrzuA|V^g#x1jjmHL z-XR)YYHUbFS=x5i{jR>GIHf=8DP|6x4zf~|7m4yl0zL|9R~$>&gyjAe%^kO~NbhF4 zFyxO60X-mpO42U2Z&(rA|7=7>&_I3Ag`vCIexGIqK|}RHy@&5&heDebbQ>Bnr~7Dq z(7r~~_V$?;QgT1(Ae;AmL`5)YabZjzi}m%?^#;8i4sEV8px#)NWAJWV5Q{PlA^aUO zXEgF}bepy}0{Mdh6OOzqeU-Y=$fpeP)bVo?Ns_+VPhQbCY>J1ksaV7MB*^P(dQ!s) z#8)8h$q<+w9SBH#M}Q~H!JKc9oF#;b_^BsvzX^xDgkymqh! zPs1u!`sT4Hp>MV_Hp*DzfqGB|=Ur=@c^P}zF7F*|?xcPdk*|hU_<8CNl`Y)PHh0{| zlu!9s9%6?(7O=7f+u6q*4>BXtj&wZ4qJa0pj`>UrI=IpmGl5xEIr5)q$O;zVh7tS-^&E4iB&16VRet)>`ae6>~^<9 z>`eD2?75ide_)2iarjNDF(EutDASvMJqnu-iD}a=Hue3)5k@jT7?O z3c$!>1{fz6ucj zIVRE=2|7QBGeQMulT#eRQw-}5Kt66jeEQSaip;vz>kFQJ@;Ap|9ovZV9FXrbl$VNh zWA_#79gwE<@@E|mFBv|N^8S+V?Z{5}sF@6Qm(E+44X;DooyqSH?<61%w)#r1Vx&8; z>)=gYU{87-^`5{u{s>&I3w307L!lRw$je;fM;pBnr;rF`2}_m@4?+Ij(C4_sMH|nB zI~4R7b#@h4V2Cqo0~nXMrN*Twud75_ zc7CG3vim!;d;K+RX)A$UZ3Gxs4eQ*`fvrG$I>)UrUI*+_*r)DTTarjX008gV61Hj4omq$2hVLqtvVkaJ4-q&Ug_puz5E^ zcTjc#<$J_#Fs?-01yYvvN_J%7h%?1uJ_Ak*p%--Owu?1G`IRy67@?OkZb5oJ<|0Mx z5@R^pRfv2(C_5=;i7^ymANaXYp^qD5RvEP@Lk_#%K!jDOqX+)GBLAwmYzrN_b2>y? zVXQS=DM1+Np)uC*HJ;u=-_UuCVSO9Ws7w4hBmWMrkL_zp#5hgj={=F&Phh?%r{`<% z0Wex%592b7{Cn`l_v1RqnGNf!VSD;Ius!`8nQLD)>k3$TGY#wC)Pe123f*fnHTxze z$2bvfY=`l7jdegCN93`>HXiB7Vq?^7e_W1bY&&C2T|(5*%}UGMP}Wjgyc^;lAp9-L zaR<%<<2D$x@!wT+E!&6q83=Dcc)!HGYy)6O zT`;z$xOK)UajPtJwG#Bf%a%Tk%?TZu<>}WRia&ZuTg5)XLv7BI7a^~ zp*&A`Mal>Y(hlRgC8Mzb@5tCyMneqRm^QS+AN@9rrXYXx+c1v5qbjuMuVDhp;(1hR zU2+lyMWElwlleO~#~*vjNc26jCE7QQg0N3=P^JmdNpDb468fJshrfqaBrTwzr}GDa z#~cV=GYULsBzVsV=zPMV^Rb@4{AWDr6YNI=Z9J(9_|OjEIah*j(t<}+daHC=PwWXp zRJv~9AyF9T%@1jz<2>gY@O4K6ZwU+c-ODb6 z=RJXZHQu{f3h)@|I;~L_B#Xtx%76rQIBJZgU0KBR9)-Mk$|fZ2;S>}Iu?55bdI;7nd4jZNQ&-rxh}Pc|N-bo})9yHw5Y4 z;NiD4I>f5gJJ}Ld9+NoiW1$Y)*=G)WS*WUrRjZCLAN6il4Z3ycaEO_)=Q4KO!OR^> zSX97HRu-_283GE}&5nDqAKMH+M)|B`&mCC%53<$WP0Xq1PBy6Les-qkb~dQ{UUs|t zKGv~Y5%cd<#8Oeeq2*xXP4Lc8311enbzClBY43Q#V1{cf9C#~wRaD#oEO7JfJ|2C3lRqR=LP>@EE}nOI=j zW)>Kl&*lS08uzMk+El~(`QW4d!B<-{)2a6vIUf0*ePCD#opH14qz8;&$wx|n$vplSg17s*1n>L~{NHAQXWj+A*r@KnCV_(K9`Z62wpi1Wsr}^nve?T{jD&S*o#PFtT5wnMo2FTj3lmX zHu4krSR$oS1$g};5%EYP2V)fcNBIW`%+SddH01<3)mV5{L#&lo?GD(PaRQ5tcV&&^ zoY)qL#260VBNTl467^Q*zgC833|BcHU zBW_0AMoU~{v2oc5jB6y$_hn*k*%;%x@c<$`uJ;5(ScVPn@B z)d)|MsNpk**J~}?4K$)ep9+ARXP6UDpx$eMy9l2DYviMtw~Vpy^|dj^3Ha0i_p-9r z0S_P67?-};BjEGw?Qt45XS^e88lP@di7I1TK~`3aGU7%EI18sLJBc>){R7`K_eb7` zoM(?4V0?@*t;Kqg+*V}m(WZx(!)CuGFpn`Jdt}?^Xaz;tfH@+oNBdTeaAiq8PWbk) zSM$M{A>_%EP+6tm$SB6TFY*9|ydzPCw-HiC<=n zMPENgI~9PJgJ-Ql`xLw$-fy(`O_Uvjw0FRldl>}QoLZHQo8rnU1uGr4K-x3R?F;9L z*z-EEXLAMCYMv{*9M6pN(N8bY75HjVb_w-R%tqr(#JxP<33Sr9>>1|s_t3AU;{-Nm zp1>wFa%D3TG|b@Y$gU1@#a!sboEur`nvbUevmd62O;#3jTDl0|k8weqgaPe@9g;cB7!qah(5li!N8W>ehH5&$Ip&Yg$l{ zhVS!+Yoa^1(&(~VQ_y+v<=MXc9Z`|po`MQvc|OvXlT-}F)oY2-n3s3JHoz=`qm-Pw-?E+t(6~^C@6aDBtA~KsMw(?$0KPmcn(qU`E3#h2KET3{8;w@}Jkw~5xANst=(lk! z=j+2NjEVdmRS}8)VuNbo%Oh1PU8Vth0r2ISQ~5hKCko%M7JRuDeEF&w6a@L6g)d(@ z+sc=(oNwjJS1$V3eEE~9AAxTdARCsLo@P+?^HK+#y%KD9Nzpo`fVBiqen!jsJYV5_ zHPS4)*sWsr7KH6*=kvBA4RODOeI@5NBW?>GE4(~=voFGkw}gFV+0|QdhQxk{+*ZIN z?U(eeICH~c@NoD%U-5hu;HwdD&9BJbj6C*m-c}L$MdTN4<)c2NZ$Z4Ze(3zrzG`2& zEx%~XZ#%EDRe~`3;A{1ZU%3@F{XC3#Ykf+DMTG6}ynYUE^Lg*f+p9qPY-c@uaqbFj ztwy{xKlGfa`U<#sQ$fbm1~NX?-H?*5Tu#T!%(B6)gRB*B?rrpkR`GV^ zS>g-S+w?)*QI{+Reb}uJYKwY2I_MA8a+ssGyvM9{1o1jt!b*DcI=bl(^|IGdX@k3J zh3f(N;VQ^aRFI$OAUlbKoMZ}Q9D^X^c(qR<(?E7|2XS?fodh7R^MFFuXKE1}(7K2z zS{Jh5*2Szn_L}qK2NCD2MwX836!J>09^K~v@Qa3GY7Chy;XJ*VRSedmjzadW zrih7dfN!B?z0l55O);B?y<};l0yY8tm=&|sn1Q|L1nAi^5Z-IkyKO*t2lmQW;+7ka zBYive;}em8V|;((@ok3nvqgE?s|YWTVa5sABli`hWq(Ef4RN^^dCNt}TIM0VJ;oZI zfpAm6&7I(ib~G((W|LLBKtAn_z9&eX*d$NbD~)z$GhOCcWYWA&Wl__+RB(^#%;hKY-`It zN8X+IpM&^B?C-gbIS2YiF0WV`W7R9hkb4=|^L_^TMgOK6*s&?f+5^w~ZL*d43C`>~ zXzn1&xQ4Q?ql{RzcPHXzBA<&*ZWYx*4cS8DG6Q4>v4Ku(pxza_&c4~+kRfoH1kQt% z9fy2ld(3js@iOCV$Wq=0tPf}-0lL9yF^i2x$SJa7jK)&HD{<$668mwH%NejYpW@=g zjzPYV>Y`y>?l9HGnO((uf{TjPB0Z&znyDaXNN(fG_F(Vr3jJR>_WDlH{}nWdf^nYu(4ikIr!j>AH6ZC&)AZKub z{_hOr3{KGhMIzrB$QcS<)jB8W{{}(M-~|2OAjlcuKR-xo(eb%QN6J(>HS~Wv$Qjho z|3#wBTtBCU{!iNsa~|}65s)*ep#O`2oIwTsAM{wz<#8Qhqv%K{Bx*vl@CowMACJ+u#bQ%Wq-Oy64b9h1jr-c459Q{{9{}&GZpA!1NzxT)Uxn+G& z;yfHbTX{oFAI{Gy8gRFZV^T`5Dak3l)(B2foxInmPY6ogF2OO9pL^4KXQ^@4?L6+A z+4CvQoiCc6meOx+tTS5Idr9?$B&9-r{#4bB8mZ7GobhbX z^0Tz#^!1Tng;h8l)z=%U`d2sxhSX=B!8khV8wwi+R@C~F>1J2K#LxS1UosM*nZ>71 ze#Y3pTL%|g?yiUbY*F}=wrtMlt9#O+O6cJDxzbBw!17caBsm{oavA*gl+di2gdJxfITqovT!yJ z!Tk?gV4J-aXE2t+Hhi7MHe9r%iwnvaEpWg$NoGCZB(PT&A)D?C_(;H~klKuUE^>Vr zo`wy%BZEDuc^Kfv11=G8$6)LIIc&odVaLkh+Tbn?Z^)Lp4|yKW09+Q}rodj=fO9e$ z!1KE-k_4q0=W;E!+_#>d%;EP>XwhG%C+BiHJv)^%?8&D&hNmZTyx{l0uHi2h+iwtFL>KSKRgS0H9`EmPB!O?uO`S)`T!lBO}& zc3*3`OM;gX0X$5_cq`jFvt-+Sl$mHVr)M+Hh)3dm8oulec&|pEv+y2_v*LwmomdfU zLhryPG#~m$LC_d$U=K_J&d)&3tAY)5N7%nhpm%Y>y5a}B?I^?zgf067)Tc(c6Ks{8 z;X^giSLK!-hO}D$>^Bt`x+Zr@;oAmoWu!S-3+5 z-*m218nN2~>~7PJvbC^(J&o}~yxD01;T-6U3Tqh-R+t4xnE~{PbF{h6&>i8dF!G(Z zgwMh+01pQ&R9o`ltZc3Z`X8LvMcgUa%9k>nxy^M!TewZT3+m@>op$C-4i67q0Q$zg z83xpO6Lb*(+j^YCMc(tad?LawxG$lU)dvfb>7LQe^gwilKXXaJm7Ub^1ot~%5vgGn@|p2`i-+6P@H;GKU>tkm zeSfTrX%yZ+!H>kF9N5|>LVmgxGDFcSGpyM;$aJH4u<6z!eZyy<$7;}HA@1@h!kr#< zc<$nVIr@it%*}P6opUH_I^xG6{tnuYbIIm_9@)9)5%0C=GfP~+B90&6wOe}bQN)KJ zJ`{0Qc!@cn+v40ho)>9_pg}(W-0m*$`&*!IcQR^mPmxNZf~|2AqYgTF1MbCefR3$Z zr-x~!;AuP|D2+FYo0@9Qa=UWa!(D+47msD1a|N~`JdAiAuGtxe{`EE8g8t75a{<50 z=RD@W0F3=mJk#-9LioiwZ~@vQvCS#Jp97(=Bzfh zxiuMc>5Fq9zaQwgbKoh=f$Bd!2M)H)t^EFx8hqPzoThMh49Afe+T!8VVQqiJ(&vsmGFAnq6OTIk6_(ma`cO$o!CJs5L#n}ugt+l9{Tq@XgVTGkX9?A)bT15RTN zaKu`1mJPw$({|xeltIiL@e0J-??Z~kyfzf`T2HJYL0Cf$W4u1b8qgoUEJCo3#Q~n{ zH9IUkgSp6wxgxz6+IbRlLn;xzK_ztEGWezdonlSl?YIN_4gkzi;Dp0Le`QX^eL}d; z2yJuEMgMWf5m|hTB=dB@1&V^X5_j*wCybe>c`oSI3|h?%L^<4-0*6xyYO@ahB|zI| z;L~ESAAxdy!DWIKmmG&2m)y4ICTwd?Ak6K)@7nCV^TH(Ot3cn?VOATy{Pwd=I^fyd zb&hEY-bLcbdS~E25KlJ3oTk%(i%;>qNYkaDl~bUtZNSlX;OJMHR+}TS4p)OluYjhH zfo9KuW(x)Tx_%n#x+B8Yb$vX>C=p}y_cUFObsai;8%-|(T*eE0#INgmIo5UTd2H)? zDd5Thx7N1KO|#v7#c8@9a2XDI%?BN-F>ZW))7#>#G;PHv_XES}(@N8>7Mk|5#jO$? z%r`M#YXyykrg^?(j04B{i!_}Nyz_N69B&I*b2N4x^@IT(j5jKdq~_cYLScdRu%um&9j&6a>p=YpmaFoyF0p8|OQ7igN( z@ylqdov!~8P4jfV7GiH?o`yS(vOv==pxafTX-A31LeuMzz6x-sC4xCm(hM+7LDL>q z7{Z#2deUt$R+{E_KyjLO_lZ{e$^IiupnIC*}tk<_E(3AphO` z5Q8;+80PlIm>-&8ez4KWG3m9WY6N!6iW!T;fcG1()!xXNFI!+$6yGmzU*q z#vM|pL^Z~uIp$g?jD;iK4rU{G&4Ga7{z4+!dNP0fuf>|)5$}_Dr-CL6@bI;)82t>y z<7!*W2(-Bv&v@JS^UE3r`jUaZz?+z-V$L#5}M8IL+obMSK&)$77xtfqCK`;FRwj=Amsj&y?k?Mfy14FBIRY3%LM~(p>n; zGG`%=20Xzuz;PUw0agQ=E(9K@1CNEENq&!+8SPVo&UF&XodVclfQBEl*o5Wkk;fZ&)s>&h(OT9o=UnTz&i4k~M~d+4+?)K{ zb*#2M=3alHfzJUo0kCiE3cn3K@y>v57V~elua&Rh;cCpqA=r0vJ|j;aR+zUO_OZSp z4K)r{`07$cb_jf)TKEzU|3cUT&;9aq_@B5B6r`g+pJEUDOL>2gz8HRo|5>^K9qu31 zXK!zUt-UqyvtJ9}`ZwTP|7PouhFfTdoj(e|TsIWYblV#8;@7Fw4gWsegCsHBpLKm0KSORRV4T#p6@qnxvTs)d{M0%=*a`NvYeh;2o3jI zgPwdjec)UI#+;8;&5Hg(*t4&{QXFEcUZFQ>Pe&P3(2hFMp}=t3x*r>eI0NFYiu#7t zr*Xax|Ls0)hhk4wo)*a9pQkL(=BEkw+ZykP%Cbm7lL`C1^L2s}{^^_xxF0?1U1plh z08(aRFJIs<;rGyMaP&h7zjn?AxZ4x{oHUtdu^-?zDQ5-e%s|1_i2P+kP;MRevUPpH zm%Ji03_%{)AQr&y5$umOnW=zVi{~!txf2HeDg)q`1~B>XgXbi;7;i@j4Xah@>u-zv zepe^s?Xra*@cYFKc(^~J8{mCv!Ta!iz)kQzx4`?%@dkykd!go;>d==#_R~Ob|e^4cc z6$*=&1k>V-;2UB3hFa8nqZsXp$Ny43W+woDGMJW03h(p$XiJri7pugZw{1LZ&3SwB zV3VGJXFP1tEn&P9@YvJ;%ktWAVlOxSclD4Ur+kG@1oufYWS?L6__U@z&f;Y#Q9m%Q9zSFQBYT27y|j*?qpm`bb|u0U2t(IY+Y{lX2wy>1 zif|u<)Ad2zf3rVig65xE^@_y%#w$Wlf%sZIm!X&wU#mAjHn^+wwfZc`1`E56sV{_V zuqb0peK}-5z(QoJ$R@A6OxB+ild7 zxIhw}>=_Z62>xEH-O5gZ2dHhU#+g&tq;yf^?6aCh_JSP3Cd)h z+1AOnTN!WTpMm9Y99}z!vi}S`hvo93J5>2M@Z3h8M*}|um}`u@O_*bhyiJ%ljJ!>p z@8WY@Z84ebGL70|!4!e{i>K8>zFFn_XJn_a2??{_J;>?lpZWn4t$w+GBR7RVO+UzZ zYq6GU;bXuHJ_fiSYJd0`=n$w6>exXa)X7gD6wpQ=6zHW7>g)-cgx=8yJ_dYU^g(Sv zH*G;H?LZ%P9|H>5w_i^mka=A{AoFLOO}s&B_!w|k_G z2hTXfartmUnw%}c;|qSQ4S23);P=*mw^M-MbcbEvQi;@j1^dV~q%mW!m%9!7p6%H8 ztN@R_5I!>%w_0r@ZAcjkeo zrq_*O3Gi3P722fp%a^f~o( zIrilmb0^561cXcREpVP*30cS*w4Gjv3rE}*#Bshp4E`DUJ+QcU*SrL>0odF_Zlkc= z!5e_PNbjKx&O`W(k+bIfjeAGAT!rvHaYkElBxHe>nXq=( zLZ<4CJ3+lkW%QO(b}iOXAHlWE7domof)aO<%F2QuKOKQ*0DO7AfoBz-B#g_Z1xj`V z&&!alc8B~lAAT`+!Y}5V@LzcyK1JWeJ-M)}%>5Dg`Vlr9@JE~59 zCgwQ!q|~N;j=g`A+#b9POI|6$p1GdLyIkUF{t@;n3l=oBaIbh;D<)si0{RlU`FPr8 z_%gl3b)&i0QTESi3g(XcZttdjfjF03mowkx?3O@=jJ|$>zSo>Sms5j2UI#5HL`Cj> z(B?+8g@<_=Ux0^qha8;W3w|N(68e6Wk5ldg)X8b$0qXnu^jA4N4tBZbal2$cA20SQV5ivl zSk9|*KGp>NhZeRE1>nOuuZ(>x=c{2?A~WR+-2byn*^L0XiQ9=)ZUryP+k3^zd-cM; ztVDFH=YE&c;bSL#$>QL=tvIg?T-eX+a(|VU^T@WdTYPQfzK<+%INxdybH1}0W%KZs zDzQ62PHLw&HO5@t15Z-B5EH*wI*9*=Z~NwW&m2Xj*%JlRJlu<8@yTf`H(wml zi|GTaT7oe537WX%Xt4j;DC0mp z9M6SmeJnl`3o+N-#$3mF|J&H7;GMY*dB%BOO&y2+k6ZF-a3XvcWuW{7lo@GdG0d{dP$3a!8_zO=vM~6FAfX8u5toIFY|4*FXwV%y&7u}VqiDEO&$CrWShkNlE8#~%Kl`Ss@IJuj8R zcS>WNXKDgFRu9;-HpO|TW;h?;9OvWZ+4o<#v-UT93H*{qK%W~9eeS>)^ts=lokK7O zx!{?OXBNKIahR_b{OU;?o~pD zhr5IM8lQnR$HtHF|MvBSuj7`qD>k{y3u*W5X}0zI#e5I#X`p>O4miEnNE+*EeSt5_ zI;zgpV;xOU>kF^Ag%l=u=B1_p|LLCNQ*rL7FhPRzOSW~i2iDP9e4Y1|oVTxy`5Ez+ zbsjdbg(~1dhp{kVU3Ua-_<5Geh|9v;3u|*a)@Bvfes9#r*L{hzbHJYxOGjc&9f7qq9&2nI&Jo9&u;%``vs9(O+NM2*Emb|_2)|#0@8euX{+dvn zt?FlT5LCtv-NH<3K%)Wr{-!;Gv(Zc1*CZhqV+QUzX+xs%W*=A6J%>i7tBe|qw!Pmj zn@q;ZCd=(~+A5Pd5Bldkr=3Q%pi%HU7TWdm>CGPUZvuR!meU(}%1FE&AbU07U9(d# zC6NYvCen%}CT^=;83vm%Jk^OddEZv6{ha+9S_8G4)q!1+RU&<3)D*KN8gBRN=P22z~wA)NccSw8bPz-0h#Dd4U_ z=I|qA4*X6GE?ejaxKP0Hdn&B5hH~htG>}J}g-+F(1puDwh57-fa`5qL=!9^l#2g6! z`@;|?*y8wk2L}>zhazqq#(?YHBGD%U?o;6Uqy$?W*OziX{NPv32I$T(HaZe@RfJ zzr^p&$}9>qWx0$kSAsa3+jSo{0D!R8HYTp@c#z>$Kt<9 zjH$ojAJQ-`<4kZu#wknQaT%+D-*SY@Y~eM*__i`5kO%LAe9^t0+hkVZn}-dm6}K7O zrqlZ0-rq{`=u3QVRAZit!d%z^JOQ#0W#MP`*0@*snuKX0|(W4-x`HC(eZ^3UBr4V_&b^mLU&a4$mipT8F&9P3mhcr2xl zjun8%;&+BX{%quY9KLDbUyZv)_&1<7cElJL;(aw9`n^|$hH&Ed#emNn3!Pu0&~SSa z;thD#!Y?!Y4Z{bI<=*cqey{dU=>6}l4X&A_u-;MatIoF1T~uYg>w=Hz&s*(x)N>ne zt1tNbzL*E`R3XiN9|hOdy?DPw9`s=^-Y<~{Ie`V7h+paEAJ{R88pr{xc1#cA zU$kR7{C9RtPv^^UrV;Pt==wIQ!M-0VkhVGUb>`BE>W64rKM84a@2X(h(D3J<4KK2He3(U+>Q zw()zFj$$t6dd$rkeF_hV&FXnNAsuG}3cavDvBN_qHn>vZSa`qUg*{OQ_c>tY18Uk2 zhM({urdt`%``Gx-NX)}rmuA4)!1rHv-Jo3tYCqq-2C~gN(9vVQ#CNIrMZU@Tc)ky+ zX|J)!G;ahrn`$!nUI%;)o@&wN`ydhPCi+*6INnFj=Rn35RauIDBh9`y&lADRAkH!e zi!qkHN3DM}){Pj`ZS$XBH$pM*crWl_TQTok!MtN#L&QH^L&|@*hLmId;A;xbW@VOP zy(q`}!NXYyW4=O|EMdNOoWNT9k8r5Oo`;{yaKd`a=lQ}k9~O^xS?4~{c>74Ksjf86 zbng@JMew{x2E)v#8>)nI-ps8@NiIMX$>7Xa> z_dNl$l&9!jm@hV|=VP)N&z3MN4``=ZdpsX2J8e1geuCG+dHb^4pff+vSp*)=e{dUu zDZs_9&OKSt!I`E%@qREk06{zx3_4Hc?E>F17&u4Ye!X9e#MQwC;MMYX zb~p8go~8sklq9@Y;mO4RM^_w7H9H+mH>QFvufRtJ9y=Y{`%(!WkN1)H2X2ki(fr}b5(2{vD?iu5@2gT46;@ph6cwu8!2pr-3OYR`( zd$|3<5Zptyd*N5uXI45yu{hE!7zF~_OHDwai3E$<|tB{%7D-6YXer{V43HTd=I(I7I@x7R*S=#1;yB}S# zx6$Md#(swPh0`*pYkp>c&mBBp3i6fl|2W4V?k%!RjQK8%cSn2!snF%01x#I9Psa1| zc?Rc?%68-1yo+y>%T)LoqI014x>C7mEz)_}XTj6rPQ$Xgg9_6bg!%v51E8nmwln@x z(=F&xZgvIV#>Z3q^&Vi0{C_KSDHu~ep8WoY7kPLt|F`4;jAfqrgYzjCUQbm0OWA#` zDAB1Qd-umv1KAXxGYEScL8&i2Kn#^>7NDr z;0XboB)v4)7mtv>Bv|mwgRWzIs*s)$%=7WGf#(8s8|b)Jh8Fs+3$>IFTjmCTJeGP^ zN(z4ii@RrG&)ndTM@aty^?YHg=LG6GfqHQ7GV0*>%txS(B-DX(QUyxfInV9iYS2Fw z`liEk6Z|*mDRbOE=%0PRhH?s2=${IG!=2!Ns(*an!^^hh{aycXpF;!ATQqQ)m4N$n z{QwJ@M<(6{O7xGn^A#ygH#kzvgZPgvzJ)a1_&seQrW5#<`Sg~XkXt0; z>5HcyWC6?Zd;xhvIphR6wll47z;@nOd>3adc9q{}t`!&E#g1lh{@Cfw1vk8% z%&Q^Km<$**&e|G=saPuh`CdbFR`>SZuYTAit2o{@2z=OYe&1Tkai(Xs_trTh-A)Y>>7&%fef;tT z(jFDYZMiO^Bf)uz$8p)qze<08pt`(=myCAjX(HRVkkh+O*FM?)7121yk2Z~|Q_y<@ z(|2rKrl6Kjq~TpkBsB8g2kka6InDcCH*4rnk%p&>NBf=?Y4%+G7g;mp^nA+a?@rN6 zXjz}E`^R&n^ssH|;^{vS9USy{JFi~%M$9={Ikk0y9%^;t+wIIAyP^zn72aKNT9i2<&4-kS4w%4Sl~n_8Qr<| zu~Wwj5;}f&)d7!tateIw&cOWLGP)W&{iI7vIk}{$PHd7%X^4=&dfYrIIp-g$ny^_x zuYdVuTTN?$P8SU6S@NNRh779vc~pRmTvj){t?w+Ok78$z$Tmx8)i>vyUdfTu#?XOB zmWN3xYv}mSQ_4g-zU40^Nr@6-{`XRrY>`pdBiogIKa@~tOy2i5=MedZbZ@lhxr90{ zzrXs&iBClSX_HB9~r`5~$I*r13AKClnkLKP|`ef_i4d+HnC~;8p zk{NLdx^VP-+e1SIau3md+v^p9PG+p%@o|Eb_IP(G$nBt@K@Z>PJ9`qwC;fxk-4lpT zw(oO)@LiEQU;FZ_+ZP4evbRJyF-4@nF*z4@RuZK-oi0CkS)dNzdkr>469neR_dGT! zC?GL^N9H6sy}tLGl|66BXm02`7caJu(3PvrMqb~gpsx#iRlFA^(uDMP#f9E-nzFE~ za@%D&$(M|8^yxeq&5&Msy=;J-P8PP_F54uf7RCM6eNrQ(iArA&Y=(@KyS=?Uf)#XGm!64pQ1tNHF)(kqWf5)h`Hbk!>`UxmoJi%3p0%oJC8ac# z;vPNKBw z(!*mFBwOu$VRRoUHQ9fT1o>Uq3F;N7cJZRU?7tmpi^CV3HeDs7{!YA<;4lpH zz@yRYPApga$L4{rrw8Xexh|!?9~^iVa#ljKny0BfM=0oSmqF1TJrwliibsh@*U2dV zt9_y4whI(}{=LShR|zC73NE-@BBO8iyAHVvP1E{K6Gx3aBB!zQ*!I2o5?Z9J8@bmk zQnBpPvHbf|l7*gAL>A7GvE1Q}fUJswZm>4HHQt+qvzNwQ_3l*1H=cbW(cy zUjOFTazq-@s*6i)x|}{~nj}-p@a_`Z`m|0?Z6BRH9kN(L89z?{IRQKLQGLJox$9gB zsh-a|Rtw|X(5Oyt8;Xgp%{e@*Q6bUF1$S;2HC9k`_XlO?8WjJrd9U;I#Z-lo_t8?K%&D(k8;s5P zx*468z7{GakE_j(O)!cyJ?e&JQWq)h4;-G9cJ0gGODEzx?O~K@hCPv!)9DXZJ5CZQplkU%-@GNE@)q)0W5D5m+im?%w_j6G zOmJ&+4^N`M-XC#i?Omdt7xUcStRkxPa*LFhL~5SB<>a^b1*(7YXv17r1%1A{`c(cm z5?bM?`e09_f{xw#>~lr3gj#M2NYs2UrEjQ%#!`bcb>; z^!#2?HIbetyy|uZ~00$Wr{_MQ`v+pF-`K7xTXE}-V#Z|etM_(ze z?0CC)SRR(+>`i zQp<|uE{A7}ls@$S;f6j$m&bmb*8Z4C>jz#a?!Hw zQGa^;7#a0lwB7Th$xZqzDEGRWg5DvT*Dzu3z@;*J>D7iRj~)Vj{z`ti zHe8^RThj*zI4CH1!jm=behQlP>O11gGl?cmGJi1k6B&Koz2AqWV~IZ7`2AZ;=ZjSH zeBZ^0#&YU)eox0O9uj(QcPIJTLqsDUUYow@sf512Ts*vJFVS09n@!yLu0U&Izl@PT zlGBBdF<&-YqM%{NXTC9OhDhsPZ{uFJktk``4jqP{hJ}BaaSz``U%A?INPjsgR5N5F zKatb7h3EB8-;rS5mF8aln~X+!o<4l%sDjkm`aPRZOKI(%(zqLsA%uA~r0U)r1^xVK z-5Vnw%4w0LnLasFLW90Dn0orlsQZpdXT94JT^$oJ+iQ}Pd@hds>1Y=@4X@tnulhhj z>Frz>v>h*}iwoX=X(pK84a476JKRxF`%?pNK3*)+yNNINZoO1NWBZl{-k2q)C2rSu z(j$?&U3%%wj=ln|RybT=kM+6ryxH!j21w}KlNv+pB{{8;KB^w;FVOl{Qw!TI5b4@{ zpXn;B^8;B&r+~jmsdwdIul`u)C(rc?u0^{CU3v26vB%&~i_#1q&y$UmW#O&E_zle%IRq5$Nj_J z=$l5=GJnGM14|@i@SRzoep^aI9s9NX__3TWJ!zEnmzz>rwd%vDclHZ3PChVyew2br ziq71d1HRMo-o@SWE)sez>2EaSjz~>swpuy>>&T*-zpk7C{;IacTNQ=wQo5%J$Qas` zXw~BmDGM$r=-|b)_pZi*FMjW*XSyEGiq=s!BqQ=4xj(?j=y=#k*_V z&q?U4IPE;&Yc3)M#S!VF~yy`!>Ejv=jK* zOJCjG9PnJEi9tD8t#RP8dy^b**;4FxmIp@OdqYm^!c8kTeT02LZNRxfE>bEZN>q6VOj`>7FaYb>H+_7H@tXYx! zEezJ4rtg0J)D3~|KT_{bJOO?sWoh0U+pu41^>*EM%o{&`5*1to{-Z;NdvUkXQo8k= zr5(tY(QEsgzCV4hKs_rmvO32ouwTeN=&?XT2~4?kcRzsyaZ>%hRZ@Dy_N99|DX4A# z?6ien0ngW7_x{UKk%m+@>viawj2`AMxFxwPQr-HSy*t8rP5<7GU-@LoY4ECLIr*Ki z@A)bAllQY*)K`gG zDMS~}zMAQhD+PYj)?|6eX{I=RMz^~Hy*(~6A)>c}c6{>P^2l*AIxxXw!G-gn?@lS5 zl~}j>4L#ZK(q0KU2CiN_BvGI@s^70!rIJvmTQ_jAjg-o(=f6G@>+`e~Ba+W!yaOHD z$v%LhbL2GVjfVmiwCHrl-cM&Ms7pkn$EFQ3y4(0v`kK~6$z5&*pLbW#q*Le4zpN)3 z{`ue)-8+h;J#=ri^e2IQnMa?mNhF7~t=H!N2>v-EFy@VqWTcyWy|Fj;vn7FtjvR`Y zQoAnAjIV*uio4Ki?cgR-S|s<~lXgT#pAG!LZ{v9h{S**bGI5ubh7~^j_5}FC_B~%6 zz4uc&se38XcVb`g@%U56C;g0l)u*rNe2-z>-R*X?WvqhE^x5s|m`tQu`_+!`^jHtV z=1AkSu)o_j!*A#}0&Ti*d10#w3Yva@;8(YA3AFpOxZ$rHmC)ewv%?j4h-x1^5nbj7D!mm_|wxftpsX&``Oys&#nm|+V9vjqOD&a^Z5o&#Jb!aa7=bUMg#9&zkB={+Bu*@@`vRjjWIP#d3!dIvvKz3 zf*W!&e?G7-;2!qn3yyh>OqWrIw;HAo!2Vf(^!*<`0w1^dgY`rY1-hAeI#yYEb8&1PL{5`orY4Ek$Y)u5+am4V`TX0v zkFoBaK5%ND9FDf$6_tiX5-RwC(gV_^)b{i7dF$Uqc_S3rHx6U|Y_mMS>@AV5&viTT zxKu`xfH7@9#C$z;`8#nBx5_DWLFI)-5L$b->LA&NdF+FcBk$$}3e@y?r-1Gs3AFsi zN;U`l!J+UQ9>3XAM&{0 zR5@*VcIZy8(^yw8P5a9Pg+M2q-nenkML|FGDLFpLRiq@BsNtc#Ft0ode77P^LFe*9 zyu+}cYWrEq;Abm|jCVs4!y18KY1gs&l?be}dv+|E=SKA1!rAMK7RzbaclDot;v=DT zt39^rhGQLHHF(9P5Xe*Aj*L3*nR?MFj(>#y%Tc@CrE^Qmn$2#!rY_xCE z83q0H^RA?EXJmBxjd@RU6(a4M;PCoe_+H)_ylIztk(ADS@IkbsPNV~mR($j7Hh~s= zIzAx#BVA~>_m-OKC zPrUzb7T6ogGR%!x_E#CTrH#`*E)hw!V@JnTU&*O+WHaSUAA)YG-Vb!@h5gQ*k`BRN zOXJx9zVkCsl$><}iQBvXFy!}(I0>z;8X> zzrSDZw*uXIr_GQ_zA|!a*R5bMnbkkLMvDH9`pH7-* zlhm(lrV@_acWn3e7ECT9MSD^(S%NTbMlb^7U`PUH9G&Akcv|oM&d9%x@fH?WxU`wcQ^PZ@#EfTG&+qYQ^@0OW$&U^!z>Qo zbHyu*M)ViD)Oo=lHR5LTR&VWunWYx6W-_?GV-qBTPHrx-`V9BGx9XT zSra_Qi@tpKzC>T>2?}|;?$|139P0T&!SXK@2KmbzUD;zyC)-cut&HixeSLE7>az+a zv3oR}^{4~)>;Ahzedq}}b}>@-o9V=BpIBzfV;XsNqBT7P zL3Y|b>0C32_bJ+)EODJegt%>%+S^bFTkP`V*GK6jKcl&=N035TVhb<-LdU#$y=JD) z3jf`=NNnFu7}!h^yK3}H@!N~-o;p*>wHGG@U;L&MinOz&lL(7gy?y7I7mt2gAXwPL zMJHT?{5${ZFo>7mXQlnI4C1iG&*^`iK;CZ8rh|cWa#&N#MiyT$&(x4I5J(}(AzEw$ zxUW$yhmQ+fq7z#M6MIuF7NL}EwsSyzmDBpEvmbqwQ*hT^RSO#7r%RsLumk+OjDOd4 z9xC~pWq!E*R9n7 z-s(!dCL8Ao9bCelea~X>>TbWZ{62jD%j$2Ofip+?#^3w{9(Zv*S`Rw!j5C(HfMp{pCp^1&%LFB$wdx z4s+ZI#lp8mti3m#+)z*Xh{vvJ3{}1wZWDo?FPi3FE8_yale7BX*5}}LCw;#jML$1b zEl^!hiTAqX@b&#)DMUAhce@nu%Ttc51KnW+ywWmsTM~5dgDXOV>?y?BsKdbvxXI>? z|E=mPG_q7==*rnS3i_;!J%%PB2(eHT|vDB37EB}m{Bc1q5oyt#*gg>xY$4F~E zlgxGed0G#Ax@3=EltUns917uHJNq2>k}=f(3BG{ba@9J0)X$pUGONY4_-Fkxe%DeQo;~yc^ zO=A_^m8w*-OZc7e@lVieQgjdek))B{e484B#o+f54S(gJmkU2OQN9g)RobE|{rv!g zd^#-n_xJ#OoQ3=pmM-w$`UIKf;?N@nW_JlR5E87qx~Es4PGrp&!p%N2Ns*@ei9>Wc z;q_OX>8hm@cfp9bIp}r6_G#+;KcTbj%=_K96YuFrzugw_?cbs7#=n7+pNpQWel^Y_ zB7qlK(oHyj-38|qN>Cr8w@#jb{;%tQuWvpIzDCIBEvnC{7jqD ztUjZ0_YwH>w=cK;W->|r>JN{LfTOwwrrsKh;Cy=-%6qG^NVsP7{#~IAV!h#e_B`~i zF9+C6%fN$n-CyR#=nO& zBGaoXedH^X4DTD);`d-0334ujI0*rB$lntgguJrp^-BY)rMd6sKjRNKdYtq ze)LXJ?Xz3qhi$xXz4aK2STACR83__{MCIa7AJlI)to;h6z?UlFe|a*QNoKFahO|9H zfAYTAIGoQS4)+@ariHM--d(?%VXhG(#v&|w*hq}dS%Ks8~Borm)>{B`7y|) z`z>w*(9g`T&&32eP{6MvW;wo5qM#WS4$i_ohW#*uNuAzU7~uJ z`;0h~9Ia}*kQ>b+57$46)a$1Z53O5=4&%O>$6KC!FUKSi9A^xfY&h&*2*!O0H4Y2QJvltNJZ9kxBeAZFTs!QOVZn z%cu#~zDh_O6lH%N{SOM^=5|&@_2At;~j;GbwE=+b>bj}l;3QPj9!j=;a*Lj_Fxhd zr+{U_>)|hryWKIqNg=tpa!ozb4DzS6#PbC7Fr0DrvyIK-z8ddzx@ zZqex+200UK!qbU9xmQl=B*z6Bu?;9t{Cbo^)P!$u%=ciCLxy46!JVih0@nu2;lG5b z8Xv!Ni9)DR&XYr{aqmYq3GV4-k|mC*tM=(K$itH0L?7f8-Z=iPc=m%rwzF*KlCJ?z zZsT;Ax&Ys+DacVt zdA_2S_FgLaY`P`r*#jyGUbn8e8GJQ{XTf+6d?T)BlgkF+*Gzo8@T~qRh1@;z_q{Ou zrx~`$t_b8N8nX+=HpRfVPb-Z`1ul-ev!bc^1bCrkH+h#a$ilvBqf(hTZ>B+-8oa3Qfp=QHfR{F23p-qhzM#AQ z&`NvcaiqB2?N$dv53d)j>8gd_W*+=c?HeJlo@W1R$8&Nr><~A5i#ox1#(UEY9XXeV zv7FZw@?n@hJi3xj-c;|e?e9cyu-{mwrnnU(?y&Rl<)M_u&ctdJVq)^P0zZ579_f zkX%^CAoSF-)h`YJkIV+{dMY^1Ag|?$VlsiRE$fa=M{Qvds><{1P~iLiHLKJ9|IL}! zXRA-)4?0HO@+C4n8wG zH;>#$0K5=y7L9sfYmsu!c)OqBL+E< z|LLnIjJbf{R))h=D%n${UQQiDeN_rA^a`bsfzfSyudanr#+NLx8TWUQ;ufu=IG_2e zRE(ns8RYG|u@z$Y9A`ZL>6s%>^oONu@A-*Js#ouc{a!&O6}Ih-r4Q*ux;(q^vlIH0 zB_H=iCU9Qs5e4l6=m!b=R*ph9c;!)5AGZhnvFF)M`(1>n{^?FzmWRzw`Qe&d-6`aZ zsO#f~VmgWIoeWB;#CtfveX{%y^tpx2K6n07NajW_o$nWs%i1F^)`4?cALq05GIBl~ zn#nf|fIqV8e3ta$UQ73t*QVh4>~ySf+!X-5-6}ouSuu-fK32-*^JEaGMWe#+HQ~?6 zu|Let1Wu)ky0tdoesANLb?;{ppZG@g-OWsLPB{Q`$aHelZDoad9gC>x#B@mT!ao*D z(snk7-uZR+b^j?U30D;kZXafmC;7>iyWt14xoNIj0X}BtBFEu`y76X3cTxuMzR0+! z5Oojm?0MI{QpnwOX=SmqG*~2X=|J;QXA05!BJZca9l08%iy1kB!1?#zR7FDvOo+D< z+K8WP(~ckxv3T_5fs^;D4j?e-V>5P|gOH6ElTv-{S!7!A(Y|Fx(4!<*w))9aNJ`^j zm2O4&e_A*1_6;Flms+U5cLVg}bx-yD!LcfsE>*fe3J_YD58b67$gzRoV*7_mis6!OC0_GUg)LUi47 zgLB5|q>yw6k$M^#o8uZ*2k&qd(d*s~;MAG`s9+4dA(|OrG3P;H5fEd3p=#^|%8i=V&Z># z6-D?BqgU_Ip|6f=J!{;3j!90NoiQqNW{`;0XOGC0T$- z*k;Ng;-6O(m>!3(`@qkC`4Ku9s!~5Ci2hWx&cR|1IHsZT>ie8nLM|#_<9v_zJAYB< z{3GD0m2VnKB3}TPfBbzt>pY7{^t9F2L+?%BnBgDtoI)1W!eYZhHGj3dI-HliLYrW4!DqFTKwoYc>9cYUQr};qsD?W*DfH3_dxc*amOqs`4wj< z{TF?EMq&NdD-7f(BKZWey`WoPaV~v<{@5PS_goh`$m)Z=z3TTE#MZvIVe~YMGDm|O@MAtu7ee71IKTg1d`}$scK77{Vuv36n=7uF2Yq7gM9($kR;|pg{(K7^RB!TUyA}MFWgU^vo*mR$ynUJIPOH`H=72(LcUijY&$}f4>n& z-8^&gQnJW7_%sisQ$9ULU$pXC>QxBb!HBB46okH^>DK25U)Ha8;MS-e{O3IVcW+RS z-!sOe&5Ehy`H8-|^eP$=*^*@62tDk!Te8v~sNmvJ+_U#+mez_g$y1r?_%`&#A$m!DKX}TGkO$dSzNpVT zUZ-Awp5<%3u3-T=f{ho~b?!xe@UvXk<1a-N!ql}WFq)*0?_9Edd$$qdvC?#X(Q!Jt z{*jw{<27`i7@s8XCe*WC4(A)TqObe$9U8G_l5FD=!9M83YmeI72jiUViFdP5&S8nk_UT+bpx%l<4{2^}$k zbNkLgA^2fFo~((t(C<34evL0>lAlK_T>S0e!_|qqOgun+wE8yBWeHtG_f?`Pa8Oy? zDJe@!j0lV}9G zEHExmNsL^U&|eeqCuZRI9~P7B->2SajXdrACB}8rJJ5~hZrte9W{}X}eTsLUKri0- zXi4fNDp_!I@7-Vz|8ivDzTZ5P@Xg4s)s=x??Y*u;=_k%98O$AThOU=Arnyv;Ms7CG z>=0Q=CCml#?Gt`JBesCpt6~h&F7YPC;V5vfMjg9P6?9{h?w9`HquV@dKDTEx$j4v( zv_1y#PhRn@VCY28oudj@uS5MFamuK`dDIvZVcc9oh)HZdt1Jq2_fO~e<4@?Te-HEe zp2gnQe#Pvw1@w@@SkH0zTgISm1GQFm(CK&kerF zv+xYCl_wy__|M&>N6iv-OHg`&3IFRW?~NR0Kl;O%68pYq6k_qzSzN;nzHjf6+h(cY zJ>&iwl6e0dMn${(fd6<-Jbq>zLLk7=9-C z(Iua-NtHz^{Y~1!?=i{utpd+)fCp2#n=XHZUv@3f;$q8T{Q2lZGaa}utFyIgd=5ir zT_F)AKS(9pDq;`g;}ZSs9ckl0Rwwpi|~MXp?!M= z&)|90$|Tf*M{KnBP5K7?D|qAdst;*Q66O-95}pU&=6-{XuL^jhP106s1o)O$Ncofn z>L#b=;#+j!s=S5PZt$9K@~wdjpJ_z3ZSwQAJ;;%Wk1)@oo;fW)AF(A6pSwNp0(68OM<<6_vur1np2aT;>;F>Np0pZ3GQ z*ke)<0l#eBpTGb;0)J_6b*;rF7V$k@!ZNsyJc7pWUB~csgVjS)GGd`4?H8*3z(NkM zOJ9Wp{MjS_OD;R=bwcEmfll<7Tc$;kqdhF*(x9pV-8jB26|J4%5JxPvE zJ(O^YPM9|Oe{_8DbM8HuQzrrcts~J#!4&ncf9rG~>Oi4NVzo;Lblp|m?%FsvdD#N) zYC*`sG}aiX-vfRaX7Q$>4}DQk`tiP$O6r(*rMiI=(s*As9zg$Jyq2A1bd^FhxAX+> zZik=qvh&|AZ#qd#3XF_qNA83_VCFS&$Kq-p$Jz~a@+j}Qu;l;zVp+6Hm>QmkU4%ac zx?k;YlcWRamt(G>I+Q{>(O4^CZGiJs=35pjC<6bY{GrY!+#5~5i!bl~B1HBiKcD$9 zgLoJIuw+Qm$n>=~>E;6za+5D5feYtTP%cMmG5YRfy)%YK*r=rAWBdgd@c;deYPIaB zKN%LLy?ef5A)vI^LoU=e#Ty&u%Uh_VriZrK7`k$30hj2DFHCZ3={fx`;4hX&Zyju3 zv&fY;H-6tLLfC&A@gHx-`x)j?{*8O7w%$KsZ#W?VY#Hs(Q0LSX!nHM5z&E+ zFz-+}r93{*Ak3+TAMDT3cO&elU*i3r5fE4x1l`8_Z+uZd{M?)yuQ+(XmqIj7TBUx% zdG7P9{SJRuBS3E|%au-~F0nhVVF6$JejDAE1s$;7R{O>P>Ti#6-a#gX{Qg**^mPq# zL=K0U?eGU2{FL3-$70^+cZa!hGK&-$wv~rOQHdFs*xNPmnS-=O_x$drlHzgqY65-v zTh9!Q0e|G>)X{K3@RF}hb#D~G&&AFjyK}t(esz&(VxSO{c$YOnyG8CxvLaUU5cC)y zq28fFF6_;P=LAae(Wv>t4!c7tx1cr5aDBj%+PT<#k9=2J+A#EG8KA@~-; ze2)Tgo?_b)B}6Ka`!p33Xy!oPPB!#p0rY{9SjT|*82HAkxv@43@pL?Rr<;B z>cLxtB(KkW;+8=n=4v(e+}zOR!xXObfES3Ll1cJM9l6}eFLn}s-evclrkVIWH=3s`#+K2^q3|~$Z#WpFbok*x9q2>4(&JJKv*<5I-}*}{j~838yR7deFA5< zTB=dWU44G#WX_b3|blxZ!)j#e8MlkDPPUGV-B#E-9&7 zx1i4b5?mFza|C*sj&hG2>fsbgz1i+ZNd2jIq|zRBeDmX}&%m4U+mw471!yF$LpWz` z5|iXQH#`i2-eR$9ddn;1L4GfU*Q|p-V{9S6GJO$)2rm3vN3)_6!$FtZACT9YIxAmp z$A_HRl;a-jE9h@2zV!_fR5H5ecjg;cLP}1L*adHFr?{Cz#g=+izBM+Ix zDyi6G5#|l^t@ft4Ig*n_Udj4)Vo?f zuV>*_f7fYJi5KtV4d*vg$=eWvr}592q~c?DSTuYFdEP5_-+{wqWizZFgD>54 z8Pr(IPsoa$$7K%Dfw$(=byzMKN_Smw4)cKTG57kb*d>De)OWfe`jvBX>(j5=n3pr? z3EmGq?}y#iW*I~1c*62V_6CIP{&OTSM+|(f_l2$&o~PXOiz){AsxhaLlTr4l4Sm8SW7m~-I=RI`KNOCBA9>k!Iz5X{^w(|4j|2~k(_WC4 zT@3u;1J$8~XW$9X<=k z(AVV#Uep?|fPeijolRSckO9U1yo<>BSZ#gz{1tpmkwfXX%A}d3eB{lCa_~+!j>KA5 zTj(?+M=n|YhMs8=d0QQR?r(!+uk_o%b-bn%8*r{Jt&wp&$kjFW-J7Q(M!xHmArEe7TpzGer#Pb@OZz{lhy}nJ>sR26N?SHC{hD|gw zA8gjS0d-&Y*`Qx}6LN9wKHDRFSY+w!!$s0J86=@*_OyH~gB)L|Q~AwJNMWvhfZha) z$gn4{Nx)C>qn9lRs@=U0wu=w9Nq?WM}d$E7Tde2@ItnN5+4d9LG} zZDb+=v*(AEPb+gTB;8@4nbL&QsiD{Ep@^Ipf{B) z`X_vS2aUW4thcfd#Xaw`Ic0VR`69_2p0WeDpPp9_HbDPBthnQ~?_>C_8jp+TkatY9 zsSMoGz$EJ!;rZcv;d7QnooYGCB=1iNc;7@`ciGPv{v_PfbHVSF7?+V-yePh`HX1(}YXZNZ>3*18DdxubhVli$-&S8cT;MlBCyqzW zN-hJ3w2lorcn_c+x~FrTSVSkwBgJDaq=3suj$i4!jX9BDr{XVq(#dnG^8Iai?tUMZ zIuD^ApXP{atGtAKeq-qk_C^XZFzkHfP|qX+wZ1xDeW-8Tm2!Ld;M+{btUSDW&>8+d1l)z5V`4VWJ^mk5bRzWzjGVyS#0gXCtfvS#Hn$w`H! zVmCNw#5HDXX773ElO;xGo2yymws9)yOlOjnv8LRWG0$_X)aggAMKK_I$2Rb-#+Ezy z*$Pk0JykSw(~cmw8T9V-ci_547y0qXuh50kXkI2=O!7KZ*}FrMNnV<7cDn>$RI5u- zPb&&M_@1?PYA5t2w@+8xTUlhd;n`zd^!47}^i*$e%<;*j3U3Dwb@ohEzVH>f4AnhD z{zs^!?UQci^)cj`ZYg-=gBPTR-_NZ=9b|=ZpNoWFyJR5Kg-tSv0J`zqzi+Isdho+)y5C(xu=pj*Znhr2ndhRQ6lc`!PVGw%-i(4L9#Pou zlnov$KO%Lsk3o`j1aDd^(a4r7VwaU4_{doa&-ADiGKeaGP)2iIT z(0b%3X#bvh{bG_b!4K+(f!B0a)|+a>zn=D8$XOe~AU|I$K3R1X`GU(*L3M2CHwtwl zW8jNU>pzRH*b3elr0Efb`{bbh=~~V!CK0dbd^ri-ypqd!5!x|f;upZ-;(X6G?1-xV|a8v&pC^xG8i^T4+|nNR*L zJ^0#{{eK)+YV)x6L+u=B81=a7<9v^I%mT( zd~`Aq@p9H4=hiH)PKX=0aqdn=nMyC_dADzg_3@*Suf?2O6o8|M)K70|DMOMeHYShP;_~uopb9_|V0d^dO zzf123P62OUcV6212Rw7@W0hiIe7-uK0F(c7j)S~$QU~Gp{Z4rCpjd-N3~x?H3+|+m z{f2orHId7V4gEUL4m^C)_?#_|BKB0oC@txD4Zi82w|f_O@7Z%>cUtGL+`m1^nJ0!y z3K{DtCsXKT^#Oy18~xCUb{uZH%z?SsZ#xfGg2!g%jfXvipH?=l*{?HCBg!v}%0ht` zJHI6fZ#)MaFD($O2YoH4;luGOz^(7|_4k(pmtOg3BS*nG_3|^j76P5rNqsv@0DAU< z1lNwSZP1hdeGen3haxfkCnHU;m!V$u=kitXdDg0YF9i;Im^L9drU!qF&wSZw@PRmM z<-@+X*ZWfrH139u7nR8}(Kt^d36G~;Mo`a967r7ifL>Z^^7-7C4-B%B-~8%#=nnT7 z(c$vo74fygmoDO*u~rLTju}V3LuUG)-3*l|=e<+TLf?`s3my9*Nhgh7=Re($1^*T7 z(NG58T^1Z*B(n^2DSOWi#^UGex*lRV7zdpF<m^ z+)VHW1+kgk{;M&cV)9UEha_~DHCwHwud>KNy@ZVqC#b|YcGKxK=;y1eR04IonMAMq zHD3?(o4C!@&q)IOhs0fS;o#jx@3{Sg;G26azsL7k1@qqe_dAXXVvhBpw;0+1*~s>? zl+T|=O4WnRHig1>6>ZH;YG)DNPem3^pD^#6Z5u5!2>se8I&m0%RdwgeBP~2Ma+cLM zlDiZ4VKiZKeIy~?k2Dr$c0=~VP-+MiLJIu%Rrq{%V2Ug zBInjzP$Lii8y)ca%!YJ){v==jV~xNk50<_dy;1HoMytW-HcE{ezR$HM|Cjf z@m(deJHhXlnb_9mt^*D@?Q=94`f%sBs3k4K@J*LaO6mfi&GP&RdtE^zCG>~g#jh|| zJ9#;_2KgT2a`E?LDop4h_cflaK^}Mghm>L9z7ECO=FS4-y}WLhxIlk0U3~q}H3b?m z5i&U^k2#7vD_+}L7E*}U^@po;^zrlGVQ=Au4*m9VLg`ZAkFBbs%M&u;)7Pb6xcnLV zSI~15_y?4a`5c+>>%L#{5|91TTmZTw>hMDfbUPv zFAk~4oW`2TD=$mXCkE}s*~dJPmu*#_^bKGVk=%u+rSIUM+O6PNI{}?;+l7-yo-j!v z=dVci4k*1rn+ptod{R&sW|17BRQ;;)fn5YZnI>#qQx7aoB? z2;c9*Tb~Z;7VztJZ@H!xnY{UlQPhHbi;P!2cK%RB2lY9npk#DCtN(e3Q?i$y3m&WbRHR%q_+e z)3i;h!Iy#8dhVuV&Y=Ie@wx3qy%TCQJMqH~bM9Mpwyr|%)0jPfN>l;+30(Klm~5D* zzMy8M483A*uu@H|6#J0UOh>LFU$#M@{Fvf);Q5d3gG;!OuhlJa5Q&99KI0qPvH^2~ z2QKE(M}aq@W_0q#krQ93bjjG3P97@oObZ!M2uH%h%YuWLt8{aF<0uI|Z2Q$hMdYS# zJ*Dmt&xRk7T$=Y(kdPbQ(T-13u}5L0SwS;+bK!+Qn_2&uB*XG@doub;6{RKVaX2B@ zW;RAPLXTWLT@!YG47y%^6W>SV{-ob-)d)kqOh00!S}sS32-}_|3%x9|Vfy&;*YK$p zwTW=Hj6k3L-1k-34)eV2O;hYT@Lla&6C)oX$51VB&JlV-dTQDAM&vP?b}*ubkf#gc zn3+G20{=6l^J9HIg&cgNzie0^exIAXyM+ew6B3?RZs@@`sb^tNPaRYTwm8KDWgEP0$~>xzH+&>3&&KFL% z*F;drd$R~p`}gouvTYcqrc^Q!#Cc5tIJUx5=$1HiEqC(>%@Gy&Hc7eR)!_3r*KQ>Z z?O_m~2Qqg%Wx=PKJtRH!eVr9*JC<{vJ7r)08QbEaXM(>=wOKp9AkWexh_U>SKX9%W0e& z`#Qv_Z$qHt2#MUN#xFr3HomW>j@ZJVv|eZ$kbur2uQT*o0D1BqH?D`FuOELpSj2OJ zMp8>8RN~jbpXA{GUIaY!#cFu)DeBD5Q#1A>m`AxFwX{yF4Rw7duWuyozrhYoR>U0| z{1mf?uGY{6PxV{h2H(5*B*aG4hI(VKH{C66{%9 z2o+9BNB=ZztqW_XuCk6h!kd9k|?^44lOH&)$1|NXq!p%m|-u3mE!C&3$+NuR^G#Dxvl_Zga_Q$^ zwsiRBT)#@TiU3baH1gXhfo~lOP&E0B+(N{M_JJJKgO58!E5}5DyI&}7R7XEp$nW)U zS%N)>Zle#bO3=wWkG)G*%3%+JW7Wz|_`k+R+;iN>8zxG)D%34O&Sc|X&@cEq`$EV~SyE+QErsV+x;L*jf=W93+p8|S0@vAlC$9o7dd5%`D`;Yob9{-q z7olsV`o_uZIEy*Zg+?6<%&LPY>zdwIvzyRk*uwJv}C4Pa4LGBTI z1oB#On zBX`#C`=@*bA;ZTuf6?!u5&EhC;TypFFXdI#E)BqU?+J;r1>Ok!dniuZANp5|WWBs0 z`ar4U`K?dZMPpyviVnk{i)e)1VsEkx^i6@+ZTGv;A4FEaTRn@pu6O6GUAy)| zADMN(^^7n`nq_9dq1*5ad?Mb6$Aw7@qjWV zIm|WfvR!c;b#ZNX%W@sO->oAz?nen?51HQTJVDg!BBOTQMCc4ZT^@vLe*;e6?U8g3 z=P+S%xk+X+{1M-ksxMF{Mtnnxb;1~A`b=eR{ASE^oRU5DU=iNy;A7=C2f%}+e*ZcT zed=*W{qjFG*pJ$JqQ3$;X0|rp>fE20SJe#`cSWCmeCS%u*;UBtdjF^z4M%?UvZzGi zRVwi~t}3!0`Gi#VldLpx;KaaF4eH3(Hvf6;!T7}_0smGR&R8QSvUIu8&HvBa%gmc) z=xedNX?Lux;O~?N#WE$4M>Gm*Fw|v`rd?^&H0VW((;K|!z$0XK9lEvd7I4=(!fL>r zktToZO}i)t;hz+ePXCKMD|?~ib@&&j`x}a`Vy=Sw?z*@U^pO=pLbK6HH1gNw=*-Pe z45I9PMOIB0b!L_O5luXwHLZ?e61e|TPWF7|&A?yB-*x>f#retF#$5wFpOIuvlk|oE zYH=g3e;1XoZM?7gKkxN?^2?aQ1>QI`ukwoue}CoPq9ODX(@yYbF0w7uUVBEz8n6v7XRVy zgR7wfcwJ_3H6!20X`(K71N#{>BqGbe-=>xxv~M-05$0RY^)EkR9?M<7_w537xvHJ9 z+qVKQJgb$;gKoBcyOU=6K^A$Lx7BtH_*z=f)!J=G!OKdNq^^eG{q%fatppsII1%=h zj(_U>?a3GdF5qf*-(`sBV`hJ%U>E-0OpMZLTloCjtfXuVFrQ|ymn)zgdTR1A56}CF z*n_~`@o{h`i@fVUZ=F$q`t8ae5%+{a4vr77vO6$OyzIMVI(#LuVkUDAeqUdLQtCGW z2FV(C`|$A(=4rxT_0P6pFHAtIx?B|Y;P78RmIi;_zbSn~2q*R(B$1mJrm(+KZug_5 z1U^d^yOSewL`^&Ho3R52J)dV5$0OhJqRO?CZ5jMW>5W(7)UbEJrGC3hGW>ttC)H1o z*AJ(8)hU7BFH5L9SpnTg=`&yNu3Vhw)fM?B&>g6UTSdx5@j3I;b@QO7)$}TIoK-+Q z+FZEr{&viBzgkt>+KBwsNeih*hcRDre`R@eAB}i>9{RU;3cmgIW=kdTKt7QVb@h5o za^c>{#BnD4&f)a}iO{!w9sNyv;j5ll6Tf4L3+B2a)vs&agzhr%yM8zNw#kimJbK9S zShYK-AAJO0F*)ESF88!jFU+;?x<_ozBIikK3ObFte=Dk?F#8}Ov-Uq5rqO5b zEFA0OMh@Xz*P}NV4X}sUi+}L~e1?bz%_4)i50PU9yJ9SmFY${A(gtsPdT{M-HR%7H zx%(`$z>m4PR$b7>JvQO_&8zzWxb(?L?S&fn#oo3L)9!;Ot61`U+=M=qyW5~=DTNH4 z&3MHQe5sZ9GgrC-dmyfxM&#ggOurW3Dad7!{zt3DhJXw0H(6F~xeXjLl5_3@>Spy% zoliT*kr&iyFj;I4yu1B4Jq7i-WAidWMetmW;2F`b1PaMs@u;@`F!B?dvp9bt=O#jN znYt@LA*HlbmKk)^pXVii4&I^@)zfSNL9VFF%>#8!I6vR-u(vLS4ZyH^u`vGv`t#SH zCRdKJi0PW;GEa~fTC}M85+jsGgeBRD*aG_QuJ#xK4+b&pYfXtXMh=Ei-B1PHvNBud zg`pvnxL1Zv7x)uG)1NySr3yb}g}^-j0ldcm?+vFvqOV`ph!Ftayme~u(z>q{Qf+6{ z{S|(_8s~bwdT;RPs49)Q+k|i^h)#VDWf0dI14mcziL27ri`GGZXl|3wTeJ=HbB_*| zhI+^lnE-i6mm`~H3)fTR*IZH2@nlVoO`F1n69 z3GWf+j)VTtbvMUphow+Rgqz6VKHT3gJu*Y7K{^~@fD}1ga5r-B+ndGUw?(SV( z&{Z~?)bB23ktIR}P!Y`$xxXvA#Z^}=dJHs~{FZ(Np1gD)X-_lH9!@(XV|Q*wBr z`*`;iJu?x6es-oqVHI?oiURKo_Rp#0AGhpCKnC_!ZT=iGi+`4CWhQsP_ic~jJleC8 zkPS<^n;wAgRcZ%h8wAtHN~?`|Th!@fsalHrD++L5q`;GuOez`Lw*7A;{Jwo-`T>4I z@UyjD?Qb80jz66FmAM^r)+}==MFsH5rwKcU;*g6v>vG>3x`6e3r;7-1_xr+I$9UFa zeoF0d>Qd<1;(Ho?)F401<{-FE5Owb}#r?ra;6s}c9a(mMyvL+;9U%?Ov)0-k7+}ZV z#iYTnk8+T!6BN2r6^#1hJ8ON74f@@qIm!R`V?KMYH~b_Bdm~5FDP85*U%1$p_IC_> zMDo0rSYH4SU=OxvL0_Nbcu@QixX3GF(krGHIl29N6yNX0-Y5F5E!FTXpUUs>(rl#@ zmSeE&BXQubgufc$@Vho7d)4Qn4oqlel$Bn_URe`q8H*em+1B$S=b{08%=aGE;SInC zzl+89ZG>-e=GQjKPW=9WyvwTwwo}P?ssFE|I7b^awg$F?Z>&<@nz7ahzE@PMK*Cx& z(YvA zAjMDaWX<8;IIX|ko(z2RaiGQLDCW-wRxk{}yA1tm=}XT8*Kb&Qpl%uV;T8X;?KNQ$ zHt~4juNv6jUZbM0Y7HSrlfTDly~pzv)K_1Gy2L|k^!JCMd@15rh1W;y2mZ<-cm_Dc zH_hei{aN(sikIrWtB_y&IkfYe1R*J}Bo!ay{`V~D5ZzWvAxF|2Of)_+NPJ=Oyc!Sm zUDbW2D4fJ*Lfm&B{9*q9_2xP74Yh0I9)lNH#6#*knHfWlY>!gt-kr#!G^_1ydISIB zOI7Rg4(vl(-{CX|9kl#YJloD7DzU6i9*ua9T+NnuLpoykeFAscO;>tgenRkgmGl?j zljyyZLh#GJ6jdDAG!6eyc3+7@EPju~{NKi#3Yh!5Rz+z)PAA!)&X`tw#~fTSe^lc^ z_#&6*#Z)k8!sjT@HcgP@NIP^e0Xo-xqsde8&}FLjEw}B29zMBfj6RNkzcfmunwyc& zdGe!HYbEwuo=W=j^8|Eg(^ns&6Mz#J)vFw>#9ZU&-gVXRd2KkAo6NHq1*bI@o;Ze%G>Ih!b zt7g5)9ek$FeRupy=pA!D_cAtPkE-7tl})!OBrU)80sSd-iYE``L*+2{cy{WJKAvC0 z;UU@c(5qS-x%OWQWRSds+BaJ8o7R_nXXff)@04d!4?#|q=ZeQl5hWUVzbVS`4hQ;A z+sfAMkASNed#ju80RQEaH>*}deLUGczYBbvHoWYL%`E)Ge_e%Qn0wW(i`CJ$f{ro! z>)-GS?34Lr<}eH%{Q8Hb;&SA0Hx)5XUdx1DEE1H|4BYZDT0pnfhe|#iReSjYe07h$ z<<9?zd-J%es3u)X^ZnzuU*c!4VXyI8pLMNkt-Vha z`&Wb3J?!`z?}MLgxoZB7B-Vv3I}SU}`eEDFug?g%_FNX%BKMihrW;=GYvK@JaEnc=&_V1n+x0Q3N)uSi2v|}7xUpl0yKKq_g zCpTR0b;rPav;E`V*YUlh_G)3b4ZVL#h_ zt{>kaAAiX(^9=g~EgaYU{+puMJPKa0(T4rr#lQ8+E8zXHu$r%&Y|M8@d#qTpANzZq zOBW6l`SD%Y;qCUEoQU1!9p62b$mxLB_|L=HH%WVIOS{|-=(TNjZ|}-E z9OD-$ZoRg1UaQM{9`CcBKmTLgYwh#+4%usVCtb0>`L?SQGJH1Yy?Wi-`wRO5Clgv8 z^2uPmI4P|63qiEY%8!#jM{oGqr(?se@b4ZSKV3T9RtbK-wCAg@@qY1UB~rJ|R;kw5 zwe7xN4NC2AR<>SC`_6c=-=D!ZZTbGO+q4VV3+p@G*zgDEKin29`SQK%{2t4pu?1D= zWp92Na_;@9)s(cD8Y4!WGbne@8;tXIvG0@R=QD|ZUOKs7?_Bm1rnY_5D!dKnoxWUo z&Ickeznt^>x_I^__8sYD`@F64`}F&F_qE|WX)7Mt{vay^6?sQ9AUtZMW*y%*1Zc*7(tqukIO?vbWn{pF7QH#z?@q1W1%9AB_es?T5C-41;}@bstOe1hF4cKYW_w{ae&)6e>lJLn0E z)ws5EYww6mvs8WPA^qc>I^RVp+}lQDem!Zvux~( zf0#F{Io;7#*%a?oR_g&g@OtaAFR?2h-oNak_^#;Ix-aLr9A#f`dz;@kp}^9yY$y~toz=LA29R_Mfu@Mqha5j;JnbApPlB5SBh>( z^2%`LhwcBC`Rko}2TE3- z;(Ljn_4huP&-ta9AMZDz51pQt_r<*))fBhink=(touglNvqgFk=XSP!7&7K3?Dbyj z4-8`esQO@K3-VCk_k^h$_AK+og}27pz)J<22A!sTCbrsDgC&SGFVU~7 z-gqYkJ7rRX^=%)%%)UT_*>V1LId->q&YyW7@xIxn!#!V{&iA2KyN24Aa=t$GwB1hT zv+$wI)fP*uDX)cH{^n)o^{_pSS^TDN|m!hIMhkE$8y174Y z;&)|kT-jG@;JoFCgp^Y3L-R9M|GX;?yI85q;fOb|7k>VWv(sifCB%Qsi>Kj@Kh9r1 z7ChHZiRnIbn2W8AvgO#d;_Nx>=S+8Pn(__rA9*TAj(miEFyLILiP#AiogZ}eE6yMK z&AR=wvnTr~vko`C!hU@6jbC>;^yGWLH(q|DIp?2(zw`QL_1Ct_xG@8M+yD<{H7}iz zvVe0r_YZq-Z;XC3BE9~{Rh*-^9jY|;t4GCffhVxyMh@9L{$X|Cbd3W$hzPDF!{{G%u zyl)y;yY6e$Ghs(e$YR#V^IeM0kKDq(pKHC3`ncZf-y_%e zd}#yk`CmNOXEFNs(0+DKml#i5o?qKHVH@XU>NqC5b8hI}-Wyjm?8rXcrp-1;-gR3| zxfRTBW1n9%#8gN=5k>Xh9Z*eKIljH2?Ly{_0p_eW?3W)f7JLyg2RqpFi~hK?k$vx) zwd@u?U>(&yt?t5T-uv*L(P&FP`&O-XjrxxBUc={K{P7d?n1SUVPhP@2vnJa8tBLT% z{I3tb`iS*bo0UJiY^=d|_eb2%XsE-!c{f*|Q^b7jKeFt5_9sL8luz9K3*RGq_x)Br zoR2n|-&%ZjHS5N^D+;5Q^L|CU8qc-tfL-F=ji^#z#%0ybfC%q0uV$a_&xS3AY+-+YcJfQdXvgf5uYbA6{5insVY}hzi)o|F7dVt* zFWj|oX7gi;a;sXWw%>Q>eDwHJEdt@Chr#X{gP4!o9v$_(4f}&{Pr9&C^}>FVx3Kl} z_2_lB@6GMgg!68RLEYczswjU(%shRCepOmUcDnsC=Z8Li=g^WVoP!wpUA^x(Uopt* zZsKzG0c%B94<6Zp^Z8fHSHH`=(fGs2t^GNVT6#HjN}E@0m5k=5uR>h-?nVDy=^wE^ znowpJlgvKmuWMiZwM`?=6JA*uR5Hh)^d7tU*m>5A8zYb0TlgLJ8~cZDU5hABX8hJM zf3iM*fBsu94ioz#Eq%JbZlm}%o3d*i^ZU^F=x%%2HyUwq=h}SElNj&TT-UA-_3t}p z!sTJ;zd=8btk;V72zC$uKE{*%!)Z(Uf8UXPtX(TFuVnsO_i6N}cRH}os(v?ds}B3q z)tBB4&*Xf(mt)`{)>i=|-f$^Z`0o1DYE!GRz6q}RYO|OMJ?q2u6`h6MdEL0*{VG`B z?OwdIthkzD$og}=-$B-8>0^crVSfLjcjwA(dDwZk6&$&NetT_nMtWoT_4UZ9MTyMA zeHx@pcV%8*d1Kn0m)BvJ?)bx)(d!MedjZAz8eaFzBU>eV`8;Hy+OZS$c;&3kuT#T>!SO?vHZ@3v9jCw7dX$|{-t?K70#77 zE%j<`^Z-_ zn*K^(zW-QjL)j~J=$BhZ<_`|#J&YeWPFl{nf;(RCO^WKoy5n@KAw#j>_a2eri&!eh zKX~Iok5cRmpXgq3XCC`vbjn@szkk;l)u0L>2?8_-{H-b;Y>mICZS%8NCvD z->*jm`;mEFZx3o@CZFaVi-OxBkBjFWZY_A^@{&#a&})apHmDW<2st-jC<#8qcl0(~ zEFapE--Xx|eCQnW!;SDS&o(;9dDuR=Rc7p!?dwnfrRpxfM|5`Yl`dcK9hH$D3%_Td z;^gHIU9K^XubWcr2YPtj=EYgdd}ec{No;#PU0M4)gODRpJX5Z z;>9^Fw(j9OJ{P{Y^u=lHto#h_hI`n@mreiHHP=ST@ZLYDI{MR!56o|`X^oxqeCK3JyVnHto_h=1jrMrXMmc=Nd@f`-`seshX0!_9J;BMfwvE?w-udEQo95_+87q$N zah%Ef<3GN8E8!E)x39Bl@ICsM=g=Yf19|WIv_pfpI#9l2!@qg4&Je!OH}6t@y@uGE z6I?oHu#Ve3a?GKQ?Kn60iaDq*dTV;8eWioo)8l_k9X~#t{p~){1tD7)N39$EWURuT zcXaxvH{R#>6fTXMvl=_}7g@7j2=lU2=6p6ZtN(AjXL8Sb$&a?oGhRzyyuH#^X}`Rp zzNe1&=|0UFeG$8WZtAt#JK29ravDBYybpA?{H;%#Ubm3JaiCOdUQZ+I~J`ChEgI<;(?aE5&s+e0(M{=m-oVeQysc1PJa zdNAeNY}QL_LTmjrn|e+D@_yOryL{KU$MOA?w1`H-=2R#P8*}?knFA zXQMQ2R?z2yKl?FtTW*S5#QCiWdz|l1<2!xBe{R}|c`+w><_RCpk4|+Pr*pi_cVE`r z-4%L+?;oFN*m?V8e&@;iRF{>jXop;7=GwP8KXtcGR#_7p#qa&{)=LUGhiZO#)Oqaw zn;gD;eU=};A2|5v`@7J0&$@qkzUYFDVoX1FzG;8-(6sBbZcm_JA5Q$>eb(Dc{4X7B z&w0L@FPumni#_&E-cRqi&SQV$*p3Dxn8zOZrZn4a!*^{O#@_yj_eTHh_{X^hdi2MtP0P1oFHm>ftljk< z=Thf?_w~0A*$1t?e8qsv*hyQ@?|$V~elH`Zu*KXp)s*U$+v=~yE|H>}^P`#@J zk8H$x@6XuO!*f{2=8Zhx_`((5ce(S@(JqH@q?aaBFBl-s}i`kEEvM2oj>)Y8q3sxm^PAffN@WXnX z!`bA1W9BXPBic>7pIODeLQWg!Pg0`Ue=M&Tc!}|{YUSNFo4?^bj@vd(#P@#sulb{Q z4*LJp?(@^%IifeQM7b$-1Y zcIFXRznryz{h^d)o3@CJBf20K6;0C zwr_Gy@TK);I#qMiH)?}QTl0BQOTnEp;)cj^Y=GCw){b!#aX536_b@Ae4-iKX$ z{OA4HNqT>K`bq(IiPUc{-7W5Eqx2jSa{u>bwn}l&H@7}t3Lkp8A33oDePhQ<1rhA; zyBzrGW{o5K&h5f^skc`0zIu-XiFes&_~mY-_v&2E!3DhOJaYx(thN5ZRovBTU28jk z59eI>wK`VFxcqMFz*`^`>;pXqDz`%{@N zW4ADW3~o7cK{0mHB?r#GGwLeeJNV|E&%a^a^KER~(JK47on9>K@TDE+PD*-Znqv9x zfXkkboq1m(v9!h7FxLIG4{khl5<5uWlPl(Q9Kd%%*Pov^f%)HQ$ia--toOW^den&i z!$$e-hv)h?o5uT*Gt&B)zTy49dJ#KMvR`pA#-~}@Zad}DSLa`v`YPwlBHnw@Sr*CxA!XpW%d;ax& zdNVCNoilq+OMet?A}%-bd0P0aSF~_Xho^_H575H<0aWE*`IpG>5zh$kHB5W{lm`E5 zzuMTA#I>2fr=`D7hQ~ak{+`%O#r3S`Pss4f`qYn~vCv{a{y%4mc0cKf;qy3@?~0*o ziEGR=@=u(lh0k(%di&Wd(!y)=Jks>9{Od2)!Z$O_p4LBEtZBr>*2YuA!^~QEKg$2K z=Qn?>g`Z;lKP^0NqZXd60lEDB>bFUAXK5k-3b$bn5?4xf3p^e$7AM~g-KpKIvha^a zPcp~?-dUQfAJ4PR@vruYRkiTOLcRn3JwB3(v~Z40i|b$IJ0rtaY2p7)|ELyl?YXD7 zPw)jTJU~nNU(aV=(!!JMpZ>hDTnlHZ|J3%+LWdJqz%$0Li^kQ8C;2~3etJF9p3izl z`uA-$0xH&`{kwhav~V$gxc(I$k4;%zs{n)BzrzuG%cXmJxT2fYMCanvROyZn!%eyH zNW0|NVi46x(tW1;RBtw<*sQ3w()L5URrVh_d|dq#$JM;bYPmY|zu=P1@>wZY;@RT= z^sIO;k-r%EoRqi!`dt0$oJn9geV2Cf19QMZiV~TvVcO7jUxO@K;gEL?z>f*yF}s{(2a7O($M#W^v{;K z6BO-|siAMW^j|G8QQ};QkziBu8z9{|ceMQWNUUVMi+WXP7`P7<^-7Tb@zVbkI+-Yc zA}GqA0E%+NYv>yb4#s~HDB_KhZhz_SCEecAom8o%6Ay}X;xzPaA^p?;(857XcYlJrlM{_)`R z_|LtlJwHoB-QP@qMJk)8NTlvXPilAH7LrJ1d4W!14TLf&uaczzq7}U zJ5yq+#5jq;5{(kQCAvsdK#^|zX$?33sNpJ!IVUxDzvEiIsm#BkUsh`v7z2uS9U=V# zr2ha=^vgBoa}mEp!@yLqF#Km?<$;Vv@u-i7^s`B?d?|O7xcKCecNrBC+DI%wJ-W z#4L%K5>q86NsN;iBQaQFfJCE2Z;5UaT_h?JD-Oy0B^F7{l9(wmRbrCFIEgV5gCzz? zG)nZA=qAxcq9U;(TjnpZNMe@6Oo^!ylO)DTjFA{DF+ieGqPIjhi7pZqi4_NB{t}BM zW=YJHm?|+zVw}VniNO*BBpM}pOLUXyB2kf8aX{uTu}ET;#7v2)5|bpxNsN&gEHOZ$ zQKGj*H;FD16^SMLwfs*=%$B%Q;(CcG65}PtN(_@YLSjFOoh7!ASX-iv#L6r!-wP5? zNX(YFQ{sAwDH7u)#!3v67yt_X^VcxYdoyQw;7LyneQSdaal2^fYXdeAQ<;XocQ;vd zc>hWbeb0dP2tTEvZys0|caDa>SzsO9`!w`j4c5lJN<-gSpeycJ4SffI&*2v9OVD@j zMvb=zfTG;}Gz{zo4i>x(ih0XTx@$}KtPP4{#6K9+fdLYY61^olfewW4Td&1Sm$+JD zqQtqNH}NJ(_b7=2BzBkB7_3G4>ucy+yiQBEzC?S8ch_qEB@$18BK;icRwP!e(c%?L zERvW7iujq*eJxdscSd5K#C;OeK@oqobf5ZMi=QKLkHpOqSAimaf^=tprp4bWalOP8 ziSeL_A1mFNpK9?_B_>IXlNbYv_`%Y>dbJimQQ};QkrKy%BEG+L_mb!-v9UyZ(9)mS z?sddZ2W{z(%^Lci`dE{r6B-7t2mM63rGJd{p9Bi|8UqUX3d2w!(i;WpNpFOPz5&wT zC@}%Mi;&ZJ4Fl^h=L`+c-A&f;l*By}S4o^Dag@aF5-%iK?0f#j8v5=7MR^lJkxzn# zfsvpn-x!Jh5_?JX1RV))4AuY@>0hx-d#+ewk;E)ewBt_cUN134V!RBWCEb%Gj*>V) zhWCzOSSyhOH7d%FEJJr2J(=ad_6z#fG;(CcG65~N{!ec=vaE$c#m)J|9r^Lpf1Mys> zTaj3?Q1$~T!cRzdw#1zh*URu#(w!i2mc&Uie2jGaOY9}l6BOlb42tq9@mjua3p8|* zs7S1sulb(=MY(24chWq~KTcwd#9)d3pvb4QboYzX;&+zVLSk)+3MkTF{hk&t1r*~p zNkiZD?@E7(@e*St27@BM0n*(M6wezq^zHYK_Pn=5H;GOX@6VO_gF4blougrr#5jpD z632idzkbr~|F#ytmqbsAjU_sPqJG)4wRk&0QNBzKeY0n2a+jrHVCT2Acr7H>mY6+7 zlhY({5aT~l!@yY3Nctl{QT~1sJ4K`fHV?N@=DC%t^-IbHIbS_9d0Xh<%4OS=rEDe2kf+D~55>q6`OSF;L zD_o1`DY3CcCyDnbY2~~Iiu77YtS!+-V&z0xE>P6#gmk+|R3uhR(Bc(KECNOREa|>K zUJJh_@r=YgiTgkiKV7;@!nF9OB<4umBXKh*;;)kK6QNrCY>7K1u9uhsium!;oi$F2 zpD8g_Vv@u-P{fas?qG=l5{(i)K?mX$hiK^)fwuHlo`$~muk-3NZW|4K?O)U6(?-L< zqLG^1W`cu|+jI>B#o2Z*(oK*!7Zm*zDRB(g0rvnyJF8V1ehw1=#Ozo)N;sXaB!?yg})7Y(~p84)k5y@r_*<2>w?N)2(n*fS-I@4MUF9Jn-=4t4g4bmj-_kbeaX6arn-6gHG=VDrF z7z~Pb93kE7TWJ2PK+$d~8v34Srj@%$!@v>lTD*P|J468$xOh2F+c(X+DW-JUjvl;Nv}uO4ov&l_4a)J~r=aMeH?Ls9<|{ZI9` zG29;>_iFsBHioRFy6mO8oTWND{e|ValI1!Z!|G+a)MdK$%kWD1T(|0TyoyqFCsK8% zQn^iBqf1(&OIbr`+Q_rc7Yx}7(Pl66CueM6UXgWJ4heNnRhL^AcyUa8Mnsn1)EnvTtzKZfa90+JTfM-$h2W_|{h31i za|*?+USOVZ?<*9ydVyI2R~7143#=^A-z@;M3-mb!WStjhD2g+jh%?w2a#9R=DTbmH zgPnf$8bj(Dyo%NuPOLSYTFY(88pE|Uh6>@kdYvJ4oq=CUvN5dBF>KB;q~~y(m~TkR zH>Biqdo9OMkz=S7z9;exr}7PF@<}x5v?1j*UJ1V$5`Qx!{l;y4nIWOfkXS}&{8dB3 zRWPp95ML_N_{~u9o1s#~I#p#jQ)MWwB9ve3I&%}uylL2Zlc0(+LuHxaZW*^p4-F{~ z4XYj!n)1M~>H%Jx?-|nX88Yv2o4v#)XNgVT5*s`HzC|`!i@=J7HkAuOUX@z4z=kGX zU=zQhHDQE6%P#@!B%Q8 zz^vD3?uFZWer6{8QG+;{?B|M$qqvrE>-m>h;pXp@4BsT;!+8lywfMZcWC>R|dnc|b z!VRam$@sjg^*8qiL=*X2&yO4sZsN3&&kqvm<7YiT!mH{Qx6L9;@=A5-w(igOCNB}+ zdftO0hnDobYvRcz{ukkA| zC47K%|0>-9(p@0kqon(L=?<3e-O@ctx<8Zd80r2%xXH6bRD!Fm$e-KTqZX$!hDCwRn;`0|L-M3_YGUfA^g!>73`boG^AUDhSIl|3coU=qrKb<@{kM!4tE!E=g zC;i@(Zqy{p70&Y_zR^u{zeV|k+j_pKgnQw(o>%E2-PZFjAMuz7x1JaKSUzt(|5Yx- zt>=AK%IB@;gIr~N>v08f>{VB_9Js;Lox~=EIScY2SThD)Wm2T^K zsx~q|>-nhJ@_FldFmyo6^VaiU95uGMt>EhEPC^Ra1 zrVqFB+Ln1?_Ma_sLj@2(Vdfe>T@zWnC@|TA- zMog!mzx<|!Pmi58J~lR*41%5vutuIREjsES2|nTVx5uC5KRz-td}2s!^t8u?oD?!W zXnIsgWMokEw6O6{1cXlyik=b_6%sxrX!?wC6Q@Pbh@tw?Vc|2Pf(Uzi{IuxDsfY}N zW=sj65IrsGaeb#xr9mD$Ee>k~S{y+^(_=$IBZ5LFMFdR<2`8?#vN0Wl!p1$}Cdbf_ zDbZ8HY3C<;;cvdyXTzdrjEfvkS6RCxXlBT?*zlU7f$Im~J{J4M{(Nmsy>Y0Upoa0};8Z>1*y*S=l zt;aEF)S$=?A=4wn!${iN)YHa?%?QQ)WNS~H5i%`Ip@+vuQ4}Qzd7eIXT5L!Z_tt8Q ziqPtzlg2;cX_3r-`D%6kXWXceFrKs)gTh(d*5oaYCsIbr9!Gw%zdQIVk$nROcI+I~ zgA&KYj*E;AjTk>IXhwId|6e!Vl*rhqn66fTZg6(7I=R8w+3Ms5XQ#&%j`4}{VL&qa ztWgNGGC)l9TOB)g>G(wSzeZC>OF|u=k`R7jgi`uW-Ga0ms-eaAp|&)6l*EuQarZY- z6v08$LSnr;QJq6gmlc2+Y*O6hh5ZX_Z62 zPkYtL=rPRL+oONuJ|07Q_4R0BZ}b?TiyPj`v$sdM-obI6;^60Qoafq7sphy|arH9? z#JdWAcb(3!Mesb=$x3y{-b!^#oYsn+<2l9P*Ee9k=1-hv#OaNHbNoZY7Pxxqog5D< zPJV}tAGj7MH5`xXYWcMup6q%}adJ%7+4}j6OLpy`ui;qS4y|ZJ4&e<=b z_hQ#*T}{Udor|B#@Fa`>R-L0?M935V6OvrR6$i(5!k_q4bTu3g>c}@>!DD~ozfWB@ zQ5S2tN*(&?Z2aPl@vh61nvRz!$KikvT&@02lzF~R?-w5&@7l*W|F1axNhed$%43al zIc%Y8pWykf*03_g*>MMT^y!oA>aN#0(ngkcxEZj}HHx}zBJ3#jycE39^#?`gc$RXx zP>)Vw^IYxuG|}0ck48AOQ9PVF>pX1i^&YmK2CQsx!z+jG-<)L#`cbio8*V|p#DBV< zkfBP(gYGHsHwb@m*=z7OnHFC6|4TTZRIpsndfw^3gvaqTS=a_kG)i=n2w1|Gf^z91 zyu@~%WxZq_rzyDQ(nJI+6QtXkzZl#i+*%(|E#bD7-(9+`?V$vPPMaOWWbk+13yK&& zTPk5s509P^8#5#JKSl|k^50^FMo*b8(-0hQW%{QViGp*%^Mbzry8au__vmb)ed0fr z=ck86PKV}$rj4J_@h`3y`TW1~y*S4%E-T+#-GcYTpH%?=r+h!_iS(?`iu9R?;w&it z`)_=2O~=jpkmP$eP-gMB!sk2&slQ?YS8?0?+vIQ(tj7PH^l7!oP&C3I-FdtLErI_^Y~F zj(rG!6+X=fBwVL=^cg~Yc+QXT={kdB;&{^6IXb$GJ?`fUuZEKTbj8Inf^b)zlVbtl z$94Ad`5KOiq;J+a%lvH|eI|(X9OU!09OHT3%=23QI>%7*pHBW-`3;Wlgmw>6zOncikPM2tOQ=RhSB*WZ%=Z{vFau`w?xn?bSH@U-zQ z(iL(3PuD-^@_)KzqR*OlrT;VMb(jD0ywF+2^{n)R2@`T|l;|cAu=tz6C*?n0xM?dl zP+Xz>iTT%>Pao;FmYX0;xV4;a(hX?W-zP)seD#d!*&6)p+1VQ7nUk|M!ZYUP|LwT+ z>C{zD*Z-J*BgTh@hD7*u^?tm@5*7Y`9hb#gtNk_aTHXKGaVhrC#ARJ)O`>lt>!SbT zxXiMo#(1LR#kk~?Ki09s-Q(-Ojms=z3vT6=ZY7npo;EJUm=tm**Kv;%|DW77mb$j( zc&#{M`?tTpQh3(5vC8+;I7d9pTH10wE8IFBo)s<%B(7(LTgTgf3a`$7w@8P!YcKMn z%<0J-4G)i=n2w38!5cx?ySSsA~m53*1J}PFT~zu>(K3KzJh3e+C#Ei!Vj%BiA=Vw;`$teGWlzcFuJ)96)&Llml%ZTLvx`KknPYRkaEhK8Z=JijzoV~1P zf_$PvrdLdQf`j0gNq;5A&9nHOvtk-IurVh%LKH%9nG0?)#*4&;9d&<_??w9JPxK9( z^?zDxJvwMOS~=7??ZKQHX%7|{l5g?}DL5&~UT3Tlf14LHxvSXp&nUL5mSG=ktSSjr zsuY{PxD6S64K(Ad!6rko>5W^5yAN(XXatpvVuO-Qu~Lfdj2Xdvf~eT8H>d;qfO@bm zXaM_xHei3ST1H8zv1-ydrRvFYq%9bGx2ewI>Kb0(elTZ9TIJBeX_P(LVjm)pM+a?= z-j8-pyEm~$+TEa($fzbUM|se&4i6osC-4en48LQd9j{=b}1{>77>krcza;IH>iN zm(vUez3QSjsD|-_(geD8>6aF(uctaaKQOJ-V85kgqEb~?aZ*?6>#93-P1JAncIqek zM(T(MMQV<|fm-*0s`?oktD_&}sBJ=wRpADEwb8>P>SDd48Zj)_9AK!gy1Z6ojx;!_ zt1|M;IeL55{z#6wEzb?34j%e8CI^FqS~^Inaxi$CoOInyt0?E)p-PpLzMTnsr0St_ zR7-m*RpB}xQyZPVTGC0Wnq;V_Iwa?ip0nz>BgZ^IS3|YGm1AC_uc>AX&NmM+7}Nsi z0<)K{gQ;BStd_r`RPE5UG3D#poA!~0y{?^>RyU=}o3e)MJWPxAHfqF!T=QUqt-7N$ z-yFrKoKio{H|JC5jI;UXZsakLcJ=uBhZ zC#hRI%0ETtpk9ATsXC%>qI#@2V!j$=tQtgpBBth=ll9e9pP!GKjr4(%on;O&G*DZ* z?KivYTAG^iIqL?bm8!Uznkco@hh9pR59zhiJE)iFe>a0x=Bsa#9`}hl5ht~pC^OaYPRVICPbwrhFUV{uw*0nJCQPyfYTlIZi zHI=owx>fxiWtzPbAtjgBeshyR2YP?cg?QE#Sb4}E_ z)IXj&esuqY`sSl-b?^P3)z2QDQCB}Yqz*8+sP@Bh%=rdqHDhGH*%LYH=aMu$9=YB? ze=bw1spELQPx=9KvZ0pRQax(kp)@mL(@<|B=Yy5@rexZ42g#hPMT*={Kf1(X1{sPz2oLVhMuMfWcX@5rK*?G!xUn0 zH+d)xRChxcQzxaN`Vne&H|0kP&B@5Au2$S|qmp!tuetrfz6v+^!C9kdm%EH#lcBcim#iYob=7Ui zTuCEi)e_oL$lWkx*k{O5vjgMX!}*BWNE`CSUiCaY=%#OP@*y2}Wcn;}kU_rNNxvLk zaV9^N@{}|+Rt=#)k3J|cmozt4)qrOYlydKCtXhhH!KVf0JjPDq**tR`?Ne<}miZOh z$3x%J)Y&3u9ZXvkPm_`UQ+3TuZ^8*}keL$raUkQjr5F!zXlXlR)e-tgaM_ii{rW_8i#qX!=!{$r+W~SrF#!Ps$ zrp{F@#J{ATv1&2pjd0I3&tzQvsMo7&^*XhdQdb>Ddo)9)`_M19Y8tD)V!oP3_*udi zS!B*pb)ZkTlBSq51UK5lfmh+Y#f;+$c zok4B$AXmNr;D|c#K_T7&f9Pl!AzXYGvqA$+r+)b!#YD?rna8!oQ%d`ynFHbHs z4})KQzBp>Gp=+dO-6}A5pj{6RE?~ZDsQ%1+xQVe)rh5UtGN|X^vMAE-ZD`K8QPj%| z4QXQjs)4R>9d6l$475k4_wsBR+$GAt{XxFD47rWsUd)k8km(HONPlEV$d(K3;j*{L zTnwKX=!ZDQg%f=E3GvgB;SU)LwW&)bJmyK-ONd_vZ(f6sENQIBGnc~)HHmj)YN5Ft zUi0F<_;jJUCS@xppK|!j+fYNjfV{XeXUFL3sy2s<%t_28O`3t!$wgmF-L2G7m%?KO z@Y;`t7RVBxCZ>Hu7=L`xg|fY9vQcbQh4vl?UNRW?daz#gq`b?}b*{lj5#)Cb-b!Zd zT!Xj#gd4uH7lzrdZnV4LGC%#EVI%g+{?gBcSFJhh*>_620}Eb~-N#mjVwG!n_j%$RCJxdcbm z(04H95&s&TcpWb6Kv?050`qmoJp0J%{;Ax)h z;X-rjDaQ4-BJ+OcMvara;UvYvNs8VLJ`tScqVlzT^&Fg($heQv!41jz<|1@JmoJLU zZg7!OnPnbAI~Kr6C6rC*RtqWn<)%tiI%DH~!Y;x~dGvV=T?gbvLErE;oq#_CXDp>0 zPRQAZJS%jV0U`}_tZVSoHRNImaukW|#-U^7G0p`i`Ot1YdykrR@J|=Yw}7<87*8T? z%w(pz^xt&m@ta~CTE=HB^%|Tc=I0#dB75|!GRD5(ztpMlADlFp`yHpz8>#ym%5WA= zT7nLAkhy3t+**d*l@Vv%A=VL$(e>^i{Kdjj9f1taM&>?3M@dndn^NGaqx7-Rr}gN` z&5)s5=;c0QUB>v|M|ddV_DUO*j=81;UJ`9r4lhLrnSw8cOcldR9;ELFA6VzL^6|)0 z;1P=)6*F&sLR&XRM^fM-C&dA7Q>uC*KLe2`50Mr!b&<5^FwcoO&JkG}OBs3~OMb*3 zNBboZ*7(6`bpZO5`bbq@cyvG=d;d4a4*d!z`6K&@BlFC)X!mB@j+)Dx8>>E|e7QUq zafEdb{d%6cuQ_vHbM)?s$Z9BDF`cw;!cG3jUvon{Q#7(CbeuTS&C~TTEkN#zYucna zvfhYD&ZU2m-5q)8ZFSWG<~q?QV(m9j=qKnd?W9!>Z{@=!8KhSR zchp0-T}-{N!AoKenh!7e!$lt;OHPanp|2Mp_eEz5$g7&V|LX$tEXLhH_^bu;=zuJ> zLq=nGcCoINX$D-^lCg9VPCcSDLl@RDcc44LOIF?KDqJLVk`UH`R~h?aO*rm!u6Yn? zTGov4(p6;glEH4vCg!gxaL6mnwUyaFm=~fqU1mJjr9Z!Bj>t!jt|AL!K2h9QyC|Ja zU(;VVHk36|Gh}`Q@_7?k8HYT*&;20$Jroa9Gh`;5^;#U|7V>iqt}=@G4z9Ay zb8yvu;fAZ;2Zek!w9hizx6CoSh3z-58FR=YU;PbjO}i;qOXlC+@If#5f!#Y3=d9Fp z#@IE+wLQ9r4w)(-pDnc4HP(+2aGM8h)ROt_6Xv!faFZWA^)7SWR>hWehQ?8YX=|aU zPoW)zo?b+s&@1Ku^m7;VbC)Jcvf!y5^lfeG=&f|n`u|N$KaZhI_OxY*qmTzDwF1r& z{XT)bFTq)2?X1aFeRT4eDFSC3_tY$WbrlX2V=Qsa{ezM=M(#qbsHG|>k(YCm`?K0DjILTPT=1qapFyQ)>_5%tlN z#k&5-Wsy$-8M75|aSwFfv$VH6{m?+^YVtwf5NpAB>?LBZibpn$)T18rU>5DQ zkbbnoPv}(^JqnHt7IFo*{wNUs+6)H>xf6PopMz?y)*{QCHQ~p<^s1K`(?26iw#c)l zODXW8(50%YMaXjnV{Wt3)}l*Ipk3_YA`jA>hYVgNd^Vib4%s?z|A^{p&j0e5AipM8*%ZSKc9umw8eMf8pZq;uX*sX7FouSYMr0B3oj z%eGWnnKr{^V*PiQcBzSu@h3cRH`*pGnz>Nu=Ry}P+EIYLfxbkq8Y1*K=0Z>En}}W| zbevYS)g{Kte)wuBvUQYxzX4}ukoN}4cM(48#a!!5o?_iMl=e9LpwL{>u1}g5?QprY z&>W47m48}jE@JKCa*nxI*fr3l#F+D>PZB6!3G(Vh8qRQ6Ch3;xTARLOEUZA@Z^4V# z;G(6p;dQu6aAO`k8Hav;4G!_7zsys)hr9e~-^A0{Qgof*S4Ay{yP{~L{c!#o(kp}O z#N26_3*q(+=mE89^RH;z6w3D&^Yz_kKbh;X4k;s#GR96K?bwhxGMO+Zr90~cgIZqG zSaqKEY)D$6^k*=0Z#{S`n04npL-j4kkuO*JQSetA>M8Vgi~bIOWs$DnueOxCl<_c- zxy(98Dq&e>KWt+{Z?B7d9l$>J8uHbaIntl{R7W>A(w+sZCC`0REltdeb>aWEjMoJE zvmI$qfp0_50Xw0q&q1z6B4f7j*AH-7i5+?;`u`4nW7TK)QFD>drLl<$+roa@$DsEy zbyo~(l#q#xeDit6OIXf9^8&{GOyb^#zcT6jgXjd4;1t0LVtrEqXSLUNFp2RSMOzD9 z>pJaHfP78G-sp4tsCheWTt2waoG1DS`C5y7^+jfdUEv)1&@#B}EV6zUF7zU;tF-M^ zbe}QEps;CdryT`yfcI=JM+F-<9S{ zRZG$w#yD}KoeD_rDCr4V-Yn_?hon;;VHYbS&97JFn@iz@C~P1Z=vk$3*f#D@psz22 z3x$n1Q7^_s1NA!mh$?rl=mTW z_LITWRDzyWf}EXz|AakjDd}`Jyl6_MFK@dFS;sy=x(krIbaanI)`LrEH(^8TK^-zE zLuqrrv>cwDjlLmlT34upuw`-n)*@T2Tc5V93B~-r71W9#r4Ge?Tol zA6iCT%ZC-3Q;}Dp!}G;d=JllEdL?rjuUo34nU{N0ANTbA<^$x>H|KzP9OK)f!^1Uh zaDtw>WEAn(l~s#dD^=yl+-2&28BQI_94+{(2%Ssl@?sv_PkG%d_iJ`+Y*%TNQ|Mji z8AIdf|8ww0b2w1gu3FO0V!h}C*R&CK9{42$*{%uy+~+xAS6W27c|15`EpvLO=Nx%B$<; zMiutIV9H;N%)gE-+(E9Yuo3m3oOy6xOZ2Wpu}0Q)Gl_Y)nwaB}fwl0&GWzB=Tvd)v zKOW8~CteAhl|ddQaF(Ay#=#-VAoh_8aBpx^?hbkdheqZ?zJ9ep){`2O(Z72*tJ4JhR4{KFiz z$^AUW_)n@AvKK7mARx6d=|cH2Jg0}9+#OvEPELlx#oGK)eKuz zZ>1e;^;+0GeBhO4rk>Q@oi;iPS2rKX9L=$5Si)C-0038P$$9tl);Fds}nLJ<~4<}#K{@vx<*@~km&}@ z>06P#49fX6{c_h(XG=c%#4UsEmL$^JK;9*<8msb!9KlZ(xnc|?(hk?*DG$~{-$}Vj z91pi6dy{B8!BaxE@(pcFI~hyv$h-sNs6At65whh87j{sPEymYC-HYg9_G&5f%XQM5 z2=7Lq>ue#NH<|ygU~72s!4K+j=ED$l`#~;o!#luBHQ*)2@|FVlM64O7GM_a^&W6#R zZs=X5^#or#VDDi4&3@rk#?Dlw18b~Wsw3?d1}|xQG0jcq*+UGc?Msoz@s!~r+?C6? zn?SiP!CeE8XM6Sv+o3B&?8s%_YHV3A7BVKq+{ih9HN!dItRM$^`so~FM%ebZGdJEK zy&G^$0etfl>D_@R8X^-y2N0Yk_A`X6l`=<8fxil{xtAh$M%vA=I?L<^Z!5)F+WzO) zd1gEMf0C{{HXNb1Ha9sL+)Q!QC6c-=L7vNyK?m|Ig&%Xs)4*6c50AE?Uxgh)%ySXU zdvk?e4~OM)KLx!$SJw*;WQ_%PIigeDF}Q5WpueKYFADkDg&iV;xj2I{w;LUyJN|jt zvI6KU7i^|=^<7Lym;*~0PfHmKb(za@87DR1)&j<7CH=92d88}yc)`$M%P!JPKtAds zV>#&bG3YGz+}}9pmlnf(EA|v`z+oPwC2U;N;gK7Rkpp0(>O~eV6E?2tV~?B1-aTNB zVeS>{jk$XCFGDBOtK{#2tTkh;xf=a$psuH>CvsQHI7y@qlNl%5DZf48j;t3)qSuQ( zJ0|L7`d!%CH_~U9;F_M~Bl7r+vJ2amuuBO2 zs|+4^=9o|a7AJ!#i-jQ`8*6Y(C8 znn;@@B9|HLr-bp0&~MAW8JK2In`SUpuft`6y99?tfPTn>(B*fw%>SF+)EW8N$C#P{ zcMW1JMKC5k$g?@)B#k~RMej*~J8GkU9Y+6dgUmTFo_aGcn<#r9DN~2!TCodj#eT@j z8ti8;1nkG2smav<><{+Hl1;YhKQ$uFKjExl>`}BLuHdX8@cPkYYy#-Y zAJc_igsx?zO$^9Wu&%kOHvArsyhbXX@D+WAY%L_M%g6O;3FI&IxvR9XSSMPZN8c)k zpN0}9_HitH4UbuLVfZrw&Wa|z^T_0RI7`^&9vYmtTqaLpFZY5=D%hK@N&PO9Uo>-O zBmA#3J_pm^-w1mu=S8-QcEAqdOg_AarY?aeGI-Vno5TWSMVx67w%$aZyNP@qWv*R7 zU7F*6<4v(n)ObqpRVia&2OKWWOZ1^1OPM>UIO9pS7@E;_x- z!yj3LG-0l7pf*Rgg8wO7I^!2&tyoQIZt8}9Y2g*tiq){s>|$?c1M8yo^s$%|Q{k9H z*ok&C)@#w8HyH0xN(a{6b$G76Wt~_9t`+M)Ix4U2s(t_v_#) ztKCcN6*@`zilbe`dNGT(t3#iBKpOrQ`4W4D&5^Gf%q<3s?j`0&!B;it(*pK&%HSQ* zzo9V)u{-FsJw*7*6L}Ik{RcdMK!I1_?@RRiP3n6`(W`GUH%7x(H<7JqC{d0cQ)9x`i&%k$vA^`t4zp@^~x9!rNewt)*M z_mg{v!qz49FDKf$49-fR+#bk-*f$h%C3G+AzM(i*B=mh3__+=8Fu0x*} zcEtgVGY90Vf_BV7&i&|zcX*Ia0T zdtQgP>cLwdGA8aa9zv;CX!=3z48oBktc#kP_=bjh2fhlX|ILbr>22aQfg2w(-s7+Z zhB5b~BxRdzSbwg}%trsWv#be+(w>5+EOQmQl`Caigp9Vr{}Nm!_Njz^bsm{M58rty zUFi#KWN<-G(mbN?%(@u)YHgENirz3#a5rniGWbQoUsob{sg7D39jg{&O4uL#k)cF% zEH~QD243w#8p-sR8=RVq?%b8Jy^DSmHgBQJihW53>?7hVgRl*W=jX|_p>^JSpK|V? z>^G2eJ8Uc`#5_g)Ya{C)9X^4rwuX9!eZmgd6i!f=0`^J1Bfq2QDoYJL7>DS^mNu)V z77D!?J*|Le-(U{?j6Q3Q%!%^2Gv)-}*i%no0|^JO!bgtiy~3UqK-q-dO~{hqBafEs z4|29koLLZigm)NsO(DB~K!&m!Yd zl=Ys$b;||h@k?R9V;#6b=;(@CvMGBr=-na6=o;oOZO*I1zG4G)JhJ11oL9Uq^d;t1)_Lz^7q5Vmgw9@p z?0X1A{=|6}!Al!j95UOFN6uIa3i+CWJgsB=2)khyNV{>Q(H)UGV7j`{y zj-k3b9UX8pY2~v%dJzs6=NyENz7Bbs_%L79J@`%i?a^`cxrS;5brrUEALbS})_~EX zjv0m69;&I`InOtY_2LfNq#Eno7f9z`Bc-aE!OOId`1g_HV&=OIy6)KC(Lmt9m znWLQHn?1;86ysIMR3SP<1^W4R#)+^uTt^0l?fw5_@6F??EYtt-=bXcG;IIlRD$6XR6j3ZKDvx`z)}w&OilCo_WSd5UgCY8?S7u;y07bft@nK*C=U|1+Q@hE;=ZfIgETUXx07=1 zL|lp@A6z7_F5^8asE1l|oPp#wk4ClbikSE?-#3zce}VFGka(fu6m2T8ua*c-5f{JV z*w#?rxoAV_KeB$1I5m(wAhF~sK5sPnWD#Sc73`~;F`B;-zpk++uOr{<8`55U{yBWs z9~r+ILHqLtd8meT_oYtoC8ozRRw?nWZ^_#)Q$LL7^DUrEA0%eQ)4y^eP5W?v1bO(% zOXm7;_H}{yCAAEKUwddzOU|vlQr;Kx z`B(tYJ|&*%)_9V*a)|h|gwInUbf)h&khoF1gDrgn$r(0~7bO?nYNsF8Fm}Z8xJ>zZ zgV#>i#4u*#(s+yfT0vY8Tjwk>(<3|L9EC%8-Y*@%t{RmEpC!*|*>pnA^LpYqv$}$Uu8Bj5z%RbJF`svmZ&b zIm9a;^2JTsRI7+vE%EkljJK11sG0Ag;WOtkcEn#uXgtZWk~~K~&)Gv>iy{xq zB({CbvAe+>*R_=G zG32rFnn=o?@R-Eh6PVx8(|)3iQLe=o)KZ4Pad9T!h4cLR`B>`t=qIwCPpi{5_F>$80dq*Qqt~_cL z9EjsN-56UC|L+Rvn#j6$2jbWnV(nIKH?>m;XBLmAXD z=2T4Ec@_0Zdt!KNz26aCbeVyE&_(iB4dcT{d5zeF9!gI&=5Uems%X+)%RVb;!|x)m zw&S>7Z=Zf^BQfxruH%*u$%p>LwM*2IX3CG$)>w$6mxyIP(h|d@K1OoV4$Mg_la8x3 z*XKyT)_CbiVw4Z{T@TX2iToyY6;4Vh>$sg82a^_J+a6RR)q3vNI98|mtP$j`3hJmv z9v^*ouj&(e5zGFEvG}FrwF=tzYJ8G*7ie!C9VW4Wnor$mW8s#jwu&+>b)%nC9<%v= zQlr&O`%hw6AsmB0Q6_It#yuH(5L@;&j=$s&V@b!$#J8=)qUU+7_;f0*iDlnX9^TmT ziM5=zY~l5f86RbBJhAL3X&C2rlrb#kbI4n#iC3G*12Me6T1!JLE2Q2mrTpBcJOz`- zFS7q4!5rdNA^jgU<`9-ye~5IdW_^vwpVZe}A^pXM7g>=QRvw?#iO+tX@(@U#iz5bw zir>iopX2zRBVT8*e~D#D+~E@C@XCtAYHqq($Gi|@4wB=U!D~*kc2vbW@?|0Yj2V2M zOT^(O@J&p&uscWb>z3(I=d}xiAmX{iPXdo<5)>e{5!-b4P~>L zJa>|`+)X?i$G;%PQ74eUZ+Wrif_gNVHlygXqojWvd9)A5a0Tzbg3tR+*ns*yd{(J< z5W8OLAkGoT1kYA-|2q91)jt$FOmd3H#vHTuWR7|nW2M2w!!L+e2X)aR&H(-S|%o_8mI$103b^BLYI{Wsq`+~|K#YT=Kt22f)1jjXXzXm_{R_LpfRNN$>Q zp}I;8!jWU^L!WLJ-&HV7^t;64bt3QNrQ4KQk@X4W{i*Ed0&&5S^mv_P@(FE&?X{8F@G2^MmwF zqRNi&+RIVS_21}xwzyG-Pmo7IjFCLi3HFn*=R<2hC4zB2&X=LgT+umh31f}zKGIa` zbC=StKcxiHu4nD9&a3emB=#55$P#o9qCg%C%8xZ{u1%wE9&%p9Q$$XJDE6enzFZz@z9VvdmHE9-`%+X z&gYGx_rGe4;X8_t*hF5CnERLH!5^p(yHOt=JEJ)&bwnK$;uZ5#Leq$bgVw|B>(0T$ z)@ofu**%W0k>fo>m&*G(F%RL|Sg&DBPw`T1_+6AkW&1AcA=({cXC=}X`qQAlS|iB& z7x*4#*GB8xnn>0!wQsyl+!&$hPMcP6!^nAMuAFNm{#*>lUTUm4C*f|R^&9eW1+l1s zdPU+>A9JiGG)qpUV`B+(%>y*?WrdXEr+D22=Ex_=I}+Faz&^4lr)9j~5ZYoFCZ*qc z72JneZWC^Uzs=YXu3LI&&+ufV#7p}KP6^$iQ_Kw zaW;NxSHD9c)-ndw#QQW<*^}?ZeskwMgD#|tyia6V1?i*kogXF-%Y48quMemf8tmYE zy$*-^%U#U%@-84B3NLS6;8-vEE1I?HZ~ReTFY$xv(m&s${*O-e2bohAo)-H@@_(-E z_jU4D7I~wd_u972xxRjk_$*S#u5Wye^BeD$=5mf=4(BRnbI#&m&RrZ-e`kiwYqV46 zHQrw+a~fT^-<8MRc%D1Y_u!mH&w5W|dcDMrUFT-jzsCODgZtK>r%v*4>0d7~A@NBi z9^y1K{g%vG)bP8*z>IpmN1ys-)NuyReUv!HxhSW4t?$G26DboBj&tM9?_^r1|2bK;fyC2c6X`>H3&-rK6OM})E7g!r&Zf>-;=eWN{Uq-)^c~f39iG2(p9y_!D z-TV&T<4gAUEbqCD_lV^9i47f243s=&J>^x!OJcO#E1Gzz)?E=hJMTScbs{~xnAj3) z0*T2-*xz^TU*-qgp&e1d_p!21$(1PKD9@*ev6rYL4^zfK z|6F7yi5T`deLSguT%+k$)`Rz#oa;6A{Rs789Wl&NcT1o`T`J7tl)Cm4E&d{S{x@^qXx$T)Em$d}z30YpIqWK!(1l~}s^$Tt zeoW$Nl3V_kHkja0E&1RI`FFU|f<-B$p_A|{`RhXa^jps47c+URg1jZR-p90y3W={> zXy;$rdDt4u{@Zhmq_*ZF$9)p}E@!{_?AL?uA~l_YagChgWM0vz`V%tNGKbcMPcJUbJg@GzR`pdKbafk7|HmFZMTAW@Gfn; zVY)75JG2gJ-o$~tafNbylz;sg3)x5cyXr-Moc!I)b3`{z<+GZpE6T}R<4A*X+$TP& zp8ZR{zMRi0a|XWP*fvoff~bQe4)P@VcEF@BtWD%sd+wVs`XlS+n|rN3)VV%2+Iy8ne@7{qp7sCGaV&Bm|zeKxK=0m2^XHb|^`X9#qofyA8O`oEg zvMqBCg|A|Xk)E{gJcyqf`U=~5O*_UxT635l%wb+8&xR7G+VcJOX-&+du$GH*A~yIL z%E1BJ6XHLd`$$Vn>B9VVyT-GWSy$3T;%qyJ8!`uNK6yau!&KfRRs|7rC%AlMeOVL4 z+-HmLw~}`LG$op{&X^_pe~x@6I^hs;Tl|Xe!qRUY<@3kgV+|7R$Ij$Qsj<*dzD(?MUA=FaIO!ORUM? z{NrD(iR7jC`Ap9$MwOSuMpu*`%(pqwrtjF;B(g=jD~S9wk@Dq4`^&_BGnmUd$NP4q zFB(q0_uie|lxLaev6sHAp>dkvP0k_4%Q+51cdJ+-xquJJw|yDo$e@p^rH`7Sq`!HB z`Zb7S{1EM{mpBdu^96 z;*ZW?OhR%?vfhE%do`3ld-~E+7k!!dF_?61qHKu%E2K`zr|e5i!a({SBF0F4MiIy2 zFk@>nKk^9q$&L2G(e-GVXU@6)gGdXR=TRp#Cyx%}IBX%`M00E^$w$%D9~B&rJH$nm=!3JUf=P6wKKZqpr~A;}1%x zwJ2gMm?+1lJ-e;JN+f*(kH&|{o89={XXzieqm6r4Vn?R0Y&?VS|H#CR?4Olt(S9oa}X)AV=6Q5IZ02!KyvMlnVUUF_cMSRNS*f=xSKZ-g^=JiaG>s$4mJS+9xA@Y#uJBcqyoVJGalDV-a_7g_@ zIP0$5Qs*?P@cH}LEymg%TBNVWA%($BOH)U%4t ziDRvfk$$4&Eyq)i`o6>1GT*5`-$Ul0c(IR-q^p*4632TdQ7@Iy&XN0;^Bxz8RbmrG zli$^O9mFcZ^~E!39}x#C_`Qy{RR!~oGFS34@#+%w-&X1qPugy^d|q|_2j6K0ZK|!> z1Qomg#4(dPfgi~m-8nujIe^5nYm8UiAdX2aO=|MnGd_Dmc+SAuLh5D7`AQw0Jab#d zXC+rKk@}IV1F7@d%zSsTtJJyu#4U->%Q`3HgxAPVi`hQv#CaUdjSG)SjAIk+sfom^ z|DjCv)jSyEh+qtbF)!-DZz*fxniS4U(-5}|jSs85B|eTn>t{5LJ%q1_U7g5V5zH?g zpp7-1Hu5&|RSbE5JNYVyv63w2DPzc2lJ9;dmYA9JEAiN1zSm;n#_7Q=F|%OW zCo4Htc6{ek#48ZTBz7rv_8!D^sk6^vtVrqzr;(2Zr^Ie(CZ4tGzVE2}J|X=Duc~`?=mFTF2_hBx>1#0Y>VkUIVKYIOhua(4N}UIGK9y_T4WVH{99P=zHfdd5P-_k(Ul}>}B4p ztOs<7F|)zUOS+S;i!?E;fn%Klu|n!|7Lg|waZcJ|C8+F0%C6*m#D=z4g3Gp(j*|D4 z+KR1=dASm+H_|4rqz$!s2XRn*yX#zof@2`{6!Y0{GcoF*P5-$l5oJ=_Aoc6fw7=uX zQ?iy$199b#9Gf4APt__uNj#0ZkN70IFPggVychi~eve}eR_eE%c-**N;??vsxz>%D zG&R#k7vF9IkA?90M8`=?*Fdb2xmqt1t4>iKqlr<=NWb$odrI`)zz6K9^apfcKk7hE ze5J1E+#+Js1!BxZzC&L=r}&J+_BL9#P&b-+pJSAVHN*mmKMLN6?mI@kc#QUUKkB^~ zRo$j#Em~*#dYoga^I-hnyKyG{r5&{CT}Wras%YBvoym8Se>p#x^Gf*M65~_rcZf|E z*(NsimX_F5%=URF)_KvUlKEz`u8qf!Tu*^Dj1`pW6h6O(cn~2vNZ~wh>Xbam^HQ&i zP31wpeT`TYLO(Hsm?bd;iBWt-|85L(k)3Fpzj)_pV>sgqMZ}`RIh+qo-V>cyOaA(n z{N$>s`M%+_rdBX&FKy(PNuRyshs|5}wyfJhKhS|#FLO1;u6jl3T-KQw@)w@>Ir&g> z)iUqmJKpy@^7&!nn_!hEd0qTH@pHwd5^OPNHCi9$bBaxMhGYB*X)YM`5am(g5+f;# zQ`uKG$Kx{b=`_bZidgcL@YDnL)Bui&tete2@+`H^o}{_tiRO##$((l(ZS%)@Z5Xc= z|1_RQ35pAdeI? z4joHf`6O{!=E<%h?cbnn+=XKlr4?-Ad?8{-4)3>yJS*5VkhBs%?N%^>f@@Ey0Sx{mTee#)Y4E;dvH^N-&WYlaY~ z|ETd|?1N*%`DH2{7>lx!4wLB5N?k*s5?FSXG?>hHKS#VuCr!maxytu%8mz0IMw_6; z*XyX(eHD=}rS2=1wv?i z_dLh^9BrIP%96zTqsglh>z_f{7Jm9zc$Tp!S;s?cbMei@1`zvcCi^pRjATBxgo*IZp*k&Vo|}Yo$tgryc2mTn|xJ48-F_OWYKqOEQk5*X672_Q?@?T#FTZTJzGn; zlDc`(-x}KEM#{B=!a338-_9I!3;C)8&r#O|@oLFoui*99DZ3Sv`AWtNT=@P{k5f)v z+2ZHftM-%3<=@Si<~H`Ri({htc*OL@l)plbgX9~p>YVEbvi~URzo*!**x-fapXpL3 zLA@5m_a7&96111f6@BAO<{F(j7Gf`zlLuzdUV5FpBm5%1=>_u20FF;2@qGmQ|BiAZ zbEhH|){qhFTq&oURNysT-lOWfYs88*%%|58qwKW7W$Q>kJ$XuM`!`5V zg3lgH*{G*&K8f}4ATM8%noP=k%$@y>dsus8q78nHcGFeHrnZsiWqyBj4($xeZ5QTa z&QsSpQP&-$4iLE&f1!-;Ch>zp`W=%FGB2P+5_{b_N2m*PxUy~!af19%M!jD_j2A2_ z;yX*+=?(D@SubcLPe{#;na{d_wQ(6PA9C#)S&KtqyQ0wB*0u6}6~Y_5S4(}5(5odLFE*RH zR?O?>dWkVq^PP389qO;p4w89%Cx|r-eCKfToMD%vnsdC!ytCVOeSK%np*X~QRPcG6 zwma3=KPEnu%u6#gx(=nS!@Q#l`U2Cgp4KOO(H{L}DHX^{0+Khi3Hm}gY31Zk9^dPQ3v zq3FtGZPq}oZ0pMP8ZEaq7wgsImQSZB`i~A=yb@^j;L;hQr-12pH z%fj>jcHCol%kkirM%&W-{C|S&_?=%l9&20o*!Hmhc75x*HKDK99l+kY6wHq|+wN_o7-3t4a4yznj>(@9v@4VKq&c*gf32cY*?={=Cw)c{2 zH~yz<)A^hRE-bZ&*E+G?$kv&^Yrn3geJqbH=^G=@CHv)l*gm$gWxs>P*5=vTbX#k* zwQS^ihmmYaWPfS7?z8&4*|II<=W+gS_Bzc*4QJ2kynSTfP5!y@KPD#Lis1ZU)nkje7Bb06Sa-oVcT{qAG~7QW}|)v@_)I$b)4sO`+>(% ztpBnt#~wVcjGaDl`h?;qe{o%RF8pm<)BP7#ESH17z0$gz^}rhHEvvK_aG~_E{AJCe zeEv%4gFnyM@?)3}?y>z~h-N^PhTgv>e{$Aie@iEW)Z$9@k?T=nc@tFQ?yyx_bQEjH55AvFRXNI@B z7P#`sKe9YONzZahS87Aqp6PB$bIMa{17#m|O^CD`m0FXuyOi2sX`7VVL}?XGZIHA^ zO>LO8>6+SZ(&lMu6Qs3hYGb9{sHshnc9*8MtF%p;+IVRdZEdQwMs01fwCUQ~?$YLI zYkNv-(bo2ocB8g7N!nf7+B9jKw6)>VD!SSpY$FD?gVWPaC@c0xjVxeofKG0|T z!ohdrpmULWyk6>w6J2(t4X`tysImlx>O#z@n2VCC$t(i7o3JO$f}j7TJX@^!&@G$664DP>ohm)?pGNu#)e=g`z^ zSDM4#d$VRd=k+VWJy!3YjORpHAuQ%jj>r)Om*e;LzD3O_+D_D z7m4O5(u%w{Qj-YFJu9wVsKuXNSc^qXYv8xDn&a3VOU4A_LYp+Knr+0}x&&Mgv!rdT zTZ2Bcsxk4n70dV6;_s%l@b(Bnm3LRX-#-m~pD^NO(#lAhT7W_eplLr z;I;UC(i)r{U5(Fvv?9eV8BgSgAvqu(mj=dRLcbt5=IZfKkt1d%lV*>2qU%g&q)PrD1C1OT!O>V-4qO0&lEg2&4}^32%I3# z8tazAZq^3uJH8w>q}OfJdN_H6qS_}GU!|wOf&3LqTIG>uFZ;(9HIu*MNUvtfO-k^3 zG>7d<3(H@Mx-lED`^V+zS`>!40SP#pZNf^2IGFmRU`MbE7WMbU(X{qB9q{KtS0;F3 z=uB74_V-4qj~U?Z}$^-w%sl$7u**?;&^MOTh zeKrMCSTEQ-BNWY!mb8*io$+B(EOz(qh2{WD+9>OK^m=O%){ti3WYyuUOD%fOOU4OD zZNqnyqLCaQhJ}7TaPqKy!~My<@amm9j3UkS>(@hRT7=wpH{inLa6B4hgkIAX4*kP0 z*uMuZk;f*kScIldyV4wsH{iw>b$Fh1`#5ktHu`o)6Z!my(LHc^Y7|Cv2|;1oFcrUE z^$RIFL0QZp?M{&AMv- z?H=ibSpn}2YIY?3V?B|OsmJU#-gtbp1Fm}QN_#xCEq?6cj*|hq(w@?H#AD>W=c}H= zHIsFwtJ@Kh{J<>@>kA1a>H$9J~K1Q4+ zFCSE5;hY{1k3qpOkq4h_6AC*&Q&BTzqp7-YLjh^&8T>q&$b)V2*_UA7==E55s}6tZ z(-l$Xcnlj9glTS}nBWtKf&gDE%67n+5G@|{eR0rP-LAAV(GGaguVK)Ri1zRt;fYT( z{BhHr{l&Mz)k*gF%+(7|R23ke@>^Rm6Z($GC{`=(zh%MV8!Hi>Rg1UXmLTTo5S(uxj(3fT&?~=oIgo$v)S~aXCHU(LRs^23pzht32=-3IpU4+)J11Z)WowN?2yT%VcdxMEgke`& zM)68a{IV7g?OlSkCM%NkA-EZDNi%Z{-}g1ifRa|~2*CW7_kLeCS#sSN!Ryb?f~ zl5fwFAI}C`ahbfW6j*SXeCjo3C1&2L#c*FEZYvxM;^6Flfe3UC!>i-$uy3Rz!brRC z$d~)4J0ho~EpjqEG1of)H*KveZ4BvWsEO*%GF&-ze*yxK*BxSS36 zV7P^NxCRd&SdBA%(vbK>2sX8kz~-^O0Zwif><9{i zvuiLWc|@Q(I1SCc>>DB;564G$Hz4<-1wX#92J4$w~ z?-XUNCU^}#XUyRgd9W^jHBM2FtQuX8xwkjKKRp4yx!qAdCJCp;8=+92$Bxn>w$uYd z6J79!S>CugyCagKeQ@g;J$8C@KuT5{Rez)?olsOg8}8Mm@Tr=MI?^q+x)}SCba*6Z zH8Mxk;m0?s;e4P5|Hu!5(Y*&{$&xl_awKk$FCL8wMP`R&?5v@Ew4(-nuT^17)@l^G z*1;GSf+lxeLrmL9yc-^c%$Q_VcTAlafw%9i#))%v#KCHmoT$OqwBKHL4#y+Lc<8fw zV#-sA*gJybUsr>$Syib0d^O(QUx)R^Y7Fs>!#8~r(Q|kjc03V?R}5ja11z~G_`5z2 z>Fba{ngs_}<9a|sQFcKMKBfJ(^7d-nq^zt@?+OodHw+pThCTkla2#mBtK%K_AX zXM1_$4>|MUJ8U`Pwpp;~z)GCUPDSOcIIMUm7}v>Hscpir)HeaMtrm>ku@d>TNy@V3 z!^3SkYM%|pH(rrg8y<_y-U)Ed?S~ecx_0&L&GW5d3 zPsid!CT-bj`-bgxD~WZx(q1X9M#7i#@%jGc_}XNFp<@D&os1U>dLp4T7N2K^!ilsx zO+I=>zg*S1rzw9MgDtT4i!J)TU?uI{YHYeaAIFrg@K^0!6XrOC!brR{b+*H%ks2&c z_QB>S?NKnVBVI8(pd{M~*0ElAY@!x-6+gUDJrRGecm|;r1=vhGuB2)lJkmT+k+TH7 zM=Zwk+o=DzG@iaIG26ZeX>UnOm~6x~^3*8uuSa?`EYz)|cUUQh)o9FGf?T)7&=d!u z$~_X(!n-2lVcL7dZ?iU)w#X99y0{pL)T=2ct(3!Rbm*UmOxms|`5RJi7j?swM~&E} z?W)?)XX~tpqy5(A%O&_>|6<&vUcEsqPa$uN9hHPn$A_U^K%B^9t`p}9oFaxT)-Ohw z$RlO1mU`94FRbVk?UP0MR+KzejeWP5;DbKF=-DP7iw1_Gq+6J4ni-q`@xMd`dEg^O}COYD1PC4Eo zcI9rPpKxFa&h<$`;`pv`8xVrg?TC?-i41Es`tDc)(~U~Z8eERmE*9i?oA9z7->ZKT zdXEppcT4RXoDCruf2SOa&s)%y^7oe$OYq0{Dlv+->pT4u@K|m)EE^SwPn|=cUAaWn z^%ILLagzL1e7qdjh+idv)fny_i=D)tfZTZ03<|_}p8qgw;e}o&$zOtB;!}z3TS9sC zrfqVP*gqkE3C@hE#NTe0!!R%i&r)wr^XZOHM$^C1#v^D{M=b2D!;*X-yz&%zYn~rI zi}ON4x)bWUdt-!C8{AOZ;}Z4doT@n}A%E?r{jsoW0_@Z5se7$>Wd!AF+bZ(dLcB&h zvyOJ=zH}qbP!9cQb12^QIcXbMtgFyx$3pbILH}?t{Uq1Ay^u~o=uT8L8m2Ob`YSflF86g*6OSM1j&^5luSg$O2pIeuxymHqSa zj%gJJc!Xgs$LT)DE{tR0V+dAlOQF{ZYu|?6`uPYXy%q(pf~Q|#QD(tHVi$dc+g5b* z2*L*XpA#sLBL=Lz<$K`l&crDIut%iy6_*`rtRyc(r!`Flpv=i>w(TDMf zRxy7Q`Rg&tqumdcm^gSjCeTKUo*j%=y+W|ooBpYbi8hiYZK_=w=HFY6OxkB5h1lnZcFPtJj)snY55W7y1&#aWig2yWH>Fwnx>=TckgG3Iy;*7+mX*O$~J#q)OK?ma3n(iI&*kc-;qKvMmU-brQCVthYD$QE_PzPez z@8+vGwnpW#M*3AHZnX$3 zrmxsu^kO_RvuKl%$EG@@ViW0BLb?f#-8ivE<+0F#i6|W29dD0G!TBN+@*LyQM4M|q zebA*dtKmbs-6q}MH?2o^-z0pKmx6#9shC@0LZxFEB5Xc#Gxe8=bW<^m@+lbB=_d@M zeL-Fmx}_1ry0uBh;{2{i9$-Rm`d2x$|A)|)ojHfLYn%b6=nE_#*#?i2=hn9Cgs1bI zRDWZ1zAwfkD)4vk$6@NpXRDt@3jM2G@?I_J>6qpSirF)L@m1;Myz|*WMzoiG8uRYNDz;=QIBC8tsb@-dKg{2Nom1Asj)*G&nvUgAcmL zVta2B))CM8?^p~IZHp<4cf`@Qu+U#<3eYyV+DD*UFn#5|sf>GA(jsPsV+S!U!>?lE216#P@u}w3+fi*6n&Oj+8BTRoH#(b9S>TCF|@_* zksnir2V+~CBrM7cLb;OmPo_K4zGfEgQ`fCqgoThxfo!1uKJ@oife0%hm=#7pY zJHp$4`=Bw^CD=rNG_dM%S|EI`HZ(8Rgb;l~4y0!t61}{J-cMGy+1!KKe7*^BAIWw&*B$hj6Kq@BP zS%6P3SkP|$d<30bh1)wf;8Oo?$lTI zeCg7OGCCf&D@tLkn1l@K(K^~{`A^y5)gcz_9=-s{Z>+^L2kKy@PB>{t8=m+Ul%J+z zR<}8V|6%CEI9rb$b-2Yi$oeb`wz@39GRn~4NofdS%ywsdFm91Yp7-p5m+x8NcX0u( zGtTyp<~kITW)-s&@y>`|*!FA`u6GH>JkK=jsjI^)GdCdi3)(Sz7vMtRTC5_Tew-c$ zzkZR3jSPm_Gn6(5<1^%^VEqDwl3o(Ojbn_h)IYQ+x1bK*jC)vaTkssmH_)7btXw1N zw82n3;t>3p3-W{Pa3#h^^@rm0o_KS%8)hZ6K}u#@e3{z`qoV?FNAbl5;@9Y(bZ99` z88a%NeR7pLbPr=49^2N@-e7ERU@&I1iN!+ViTK)XYCMvdO`iE^`8o`FYb}@3SUG=BR?((HLmf9zq1^FJVzgX!#bFo*TO(LO&gVneZwPh#?g}Y zQe+hR$HZe?RyQcMI32%ni#e@cpc&c;)-rhT8kTuvt`^|j_l)J8gGuIXvH7jD^>iVFYn) zo%>RB>l1_%k7^sD-MVAy=v17XWkQuB&%bL$&c&tZxV{{9Cl)itwhUbug)FS z!N3?sH^wlY?1*c`gxUg6l%(t7HNXphx7Vt6_A?%Ss3+Z`Xn!27m`S>c%~pVdFa<9# z9+W;}Dfz9Oex?ju}NGig>lFBq4+ zQn9AEGZv+!;MCvj8Zf;VKD<|pk>{796KQ5@vf}-BmZ3lGh;}nfC~!)~Hh_89RlShm06uWk*^HRJM5Lk4Y zF*V7pyh&S4e9yN=FT-ZWey-5IeVX>|&_N+6p-=OYZxHOYde}c?kFyc&NIOS#o#B8t zbK79`=m3ly?1O_(YLMu=X^{A5%UqlgRXGU{S4}|!ZML)Yq3o+C;)5<)T*|4$F7n!q zZOa%7UyMz?jQC#D9oo@BxapRLXJ&W7DC;tSx>I+(9R0H@QRlW4=_x^IWbAv6F#)=q z9(aCgXFPYe63OJR`!6iR*VLJDjQO}s?1hARJ#oJ@4qrbMj<~>NtRZiC%qqvRFDg+= zI+X-2Luh6<=trgCSW!Gm2PdPrAQqQ;?MjRH?NTJR>{9ZU#CX;OlO_e!w<){QRxr-9 z?sg^4=Z7G;eIhmwO~u=@7)uC9Mh71kl!SJ`BPMS=(xwCMj0(VLZ(kHn^M_%C0a=-z zXb6z}+>`VHrsHYiS2F#}lFAa8lH6g=S%>>0XlK4riL>OTsPJAG{7ee&&j>?lfCHV((7ko|{=ypk?S)EsoM3E_ zxu$#lyW`<8vAB_u1P}Vuj?{&3uHusTYEEt)j_q56w*%R~HUWP03+jk($A)*sMzawa z`ZdhKRWiS1#Xjbi7TsKjT$ebMFt-%fF&O(i0L zV!I%q{vH?swsFSLTG>rps?bOFq~`a;xczn$s1#^_TL&k}LjC`Cv6)oc{0m;5fPNzsLtm zUiZO#u$uQ}K3L5QKadYr^TQA1gC&oAQ1ZcZTk^rqk`JEhiHlGFoDYs+{H!7WKj(vu zwtR35^TBJF4_?Z6{#p7?j?4+4rCs_{PPlzbPB?}+VadUWpWK=kzR$eyBIbopQCI9x zbHlx`U|zHuzrD}=u*bch^TY2mKm2#b7yWJd;nI>cteX>yjhXaosaqsRypR02p1IF~ zwjA*veA4jA>}b3jACC1V;qg#4mJurXVan%&`Qa(H{BR`ktGIsz281RdVq8mpcwx3b zJ_^?%v|C%%{#%mnfKR7(!uQc_FmkppZif2cn;->eiK*Y%xuB}D00XP0F+V(uIe@v) zRZl<&^TWp6YRnz61UuiTKpJBj?%jqQoMPxZp5^N54AQSR%K=|5HD=!GR%^FjrDn-`)# z<=44SGP)I{BBmr9>zJeB@AJ{tVBd^yNIsws>Gmq=HfsM8gqteh<xt-yXQdoYq4; zzXx$&uL(w8K#w9d55+uynhyxBP-7DxvpBRJlYQ1C%K~7%>zUHec?643*QB4aD_U&v$GoxSLVa5st6tx#aKk0 zIj$-XPxb1Eyqp@mK5P-=9976TFdvWVqJHv=9iqcAlK zd}}^3=u7p=s$sli5gaB5;$g;KM?Ov4!`+C-l^%HMP7U6?xCqa`P(`}U#|w-LO=b=t zYicUYCB3k2UJ%EFamR9PL(t0kI88faGkvIIU(~QBVG-5`R$;bxH(Vp{|9NCGz9>jT zw>d$qeF#zG{}Tg(ims67f}I!P3hk`q&?;P^-C!!1kE3I&arws@Y{)mED{Y_XKC$SW z5eyCe_dVHq($5Yd&$!|S$Lcg~HjhGQ6nFJN&PZS8mOb%@Clp+>^Tr9^R|i$oF7vLM zjT+K!Gi{Dw#)u~L(jl3&%Ne#9$F|iWk~z`R{J5WdV&g;I&_sR+8x^Vg;RhpwV0L9b zyS5G+-&{mpT8(pqYf$dG82``)!NnyUPa31pHl`cn^Om&9kC0dI)xe9f+tn}B;r)|~ zD67@*r``I*_*D3)vMPRiJZltqjCA|c=QS9>nC&N~I(W_Qj%{P3@MGr~%t{x7X9YtIlW`A6y4J=0L@7cc8zSO`5%_4@M4SgNw*-htnDBY!`^rgZ;7j zVS6k|ZikbkaX}vk#1{JCvWEx$Fvb@NY2Il2xCUXgIfgm4MFVZJw)8c(lVYs3xeJSDpYgN(H+;7!}(IAgb8lh>Ye zSpt{rBvg&>37^t%9HcCrCvW;|5|BrJOTM^-H61JP0qu|6cPpW1tTLFis++_5g8@eL z)+Qr;#bVqlsl?yOXJaX!7fdVAj(%I0Ilb^MYai}1c4F2ft2GKwN{s?}tbp<dcE|`-_yO*|0(`R-K z_h0SC3DxItt(_Dis@2)}NgjBR;eNc62B)*PHLFV)%B(1DL?aVb;dwi*iV_TWm zbbcrWF-1M_bYV0`#U~+!d{sLxj`nLxk>oPP-dw~SW@vs5j*MA@-ajtI5c1Me^3uL^ z6FeLOVIqHNBMi_E*5c)awg~pK!}qh?vAkm&w43aYW4+tq=0iHn_I+uP(0r~P^O*E0 z2Ug8RF7bbL#S{3jYCK+^)Cn6IbNpf0YK$WOryN*-^-4F@?{y#91AC{YVAKp^Ec4@{ z&vw;T;D@&spp-Ovoj&F#E~}v#$=ob!5>OO{d0k?VPgyxdernKkgOPO{w=b-w-By9P z6AMs6I{DM*dtz)H&i7+JeGuzIv?<7|TL62~>8&r8W9|Oc@Cd9xTkm+hHZTQG7sa9^ z(+JK@Q}yI_>dB_+J`HpAtDz&-FDO`mp^vfVg+65)_dt~D6OfTD{%8_T<^>^as6QOd zjyPg!hs)DFaI?fo#keGAAFLVJ9*+#^fGr&ucO*ZRxj18BWigzqr{X4cqz7}BVf2+Y z$J^s`<}4$I)u3n_>$eUp#RvHjKl!`Hu+FMpj8AUisVR){kFu7bVaHN@bc4E!dCLr! z8uVwa)n?b8cyewab{UxOJZ#sHH8}<+?ykm<7i(~DLm76n-X-_l3XGlH6C+AZIPBOJ zJ^KeD*SQDgv(`m&i5bNe*z?6|EZJX!m4RhQAU}*Ik38?w4SPnK)L6wwjRT9j&?+IxuzjTtJyR~N*ywbZVF+cgYA11P{ zg*L?7tZR|_uyb1G_GgpdNVmUlUx@Ma!OOGiaES3($Jv3j_bh4K3{e;!9gX5RBi2)A zNlnWY#;?~&T{*Vn zq?x;q372R?G}|HA{}k7iHupqvnFV0 zn;^!XLok8zTtBcY^3ANBNi|?^Ht{{eA7^PloM8^&(|#^^)2$7>9B6xmcwy%}Ep~c# zK$q$Qq*D&xRPELhOeDS!>*a&vjE$`swgk~zB`3ZZ?~PAZ>wvsi8`YUH!8M+Vs2G}r zwzU@4G%i+i;$~vj1nR$co(RV4%!w}w4?=I(L^QvrYj|cTKO~60dpG=%`cLw$Q)x5oXH2Zez9s0%ocKqSsfSsUb(XR_sbwN5p%(O|{u6t_gZ9D+)|?$ppl#hY7Ms%p;o%&L44)V{xa-lc zuLEXKPRXdI3a%i{)hhjqmgv@fZ~+h3I7 zEP2O+_UGI&iTGxC7~b}e#aXo$syj|C&1^7{Uls*dVM)OfOduVUn`P)q472(KVV{c; z6Buh&m?PYj{1unhv844*`H+6}{8UZyLADE0H>HU0Ek5{{Y!{_Ar+m(Kaq4dR*FXE< z;;%1F-K6^E&(S~sl-tWv$Mx99wk$O-Y1c3M;OBz|(Z9~3pFNQN_kij5Khpi*+@jw( zx5(RHQ9QfmL+d@Sz#@BTm6;LHBH82MjOer?{+JGSG$HgD(;w)q3UHg9MT zkF&-<#=?8BPTwKV)~4H9qpfA59tYb8=h4Xg8JRcJI=>*2+cM9k^}en1WLn>2lkIrx zd-FGS)brV>ADMF`*SEeGzsh}o{`{wTFt!ODzdjeH^@QJ^57T<)19M`2VaiJDSr5!_ z`K6gH|9aZL&2CW?&Ton5RFTS5Z2l9Kqp zeEwyA%Y+%TCXOu`H?3scSkCF`Ji|u0Uz*qQju6(^a)QijN%|*3x9o5If39tk$Z^{_ zt=p~hT4XMXeE!>cEy1=6q(2dgY<8sX`d`j#32(h|t7P#xW!Gf(XPXn(dPE+N&o^=M z*n(N(rcYEixjnS+NMSnLC*1a)*{wTl&vlNibi{cslUol-`&;v5Zt^-Wo)=C*xXE{Y zaGs3JEs{SGeL3g<<#V_6&-Hh8O6?*o*UBVZ5J__HmhOH{=3?I0X}SKamUBdPoGWU_ zIivQRJLrd`s&fsUx6T^%%HrHn(QnN9 zy0RkIKmV0kefy+GWEy)%3{J?17*w1Y5$c;B5gpt+;srZp=8qF}5f@9f5#N?*BEEA} zc3yN+hIZg{+}7D|xuNsia-H)mFLS=-4?2e}*L04Ycj>aFS?9Fns?L4ORE?{_xuDU^ z^=S+lqmP)g6=y?3))Dh|jn+_@e8gO=^Ea6CkCe*+#nVm_kL7>WlUF?Z6s8NN>W z()^{iqhZ~oFU@Op9St|ed}+R|^)(zDB>USNM)7#5*2nO6!I$Ru*w*p*T5VfH36Ga* zy$wEcT+`OjiN}j|23~)}d|BJc@b)0ypV!O%kLa8Y#|qeL{dxTnbB(s0A)C+V%GuFFN0}dqd8L1^8SrxXPvL1VPd0si^hk~-)L^+`>q?@XfD&a z8Ilc(rw`BRrL!yi_{ROAoCoPGG!fbejf7T0GohW(P-rPMz2KF;bE?MKP^hyvI7cfH z7qp&+*R?u+*F@;GdIQ%r+S$nG)`x2&4rm<=Wm-pm*G2SKTnrJ4lR+PB7r}Y`JNxVO zhACQiZrewkeku0_Z=Bl>5uD$@lk1BcxE{$)C!OqbjJR9+5c#Jc`KT}XsSo)o zlXH(V>d9xSeW2(qxx#k}c~Ncc{(1W+UX^uS|Aklo+iUIqAFh2+u4FDKKO%Pj^s((% zid)d46;*yZr^1FJGUtt-f5H^mQTH3i*Zs!vmw)5F z6hF;_KE|C7+G}OD?N)w2BHMh2Rz3QbY_o$_J@^&3-TBx0*=gHu<;VWE?No@&^yA{kQVN7~6I$e@(Sr-zrazw(VB= zOt+oiDvvMOwp-;}89QOtlf|=VjQ!b;>KdCgws7K;Fw4vcy* zPW?aj7cb}Vs~_6mU6Wc2YW4r$vcHqNc2n)~f3v?!W)#dGH?gGE@cvi3Tg*!z!Ahfg zTSe^d*16&>d;akNf%9*s*(-=Wt%G_@spb7@%on>Y_)7N+3MK#VQbeQF+U~TlE!DN(epjzuYTd!o3i%Id@eU~x0LJHWVXbbNTa%z z-_L0Iy^On=cE3Oe|C>g;^oD4mgQL>WN{0)4e~w2@A!)%e^8Qxm61nYP-mil1roI#3 z^H6Vjr;(q<$UBX5jm&zGZYCSujw)An3EfOe?KY?M<^S+huB3BKnr-*)zx}_bU*>1i zZS*_+zo8%JVJUX@q-Q_24h=G2Qc3z(dX7=)xk;sG4(;>opVkHvn*K221KRK!`heQ{ zy9<@tt6UGq`%d0xiTqQKv!13&=egRiJIj{4IG|iXu z^;_*`&66)*kM`XO$qdM~G^J%+fC_V6-1?K8x6 zZorUDeu~#roh5gm-$3UDBDUg9`p29 zyaxFVa^9=)FmGc&M|f_IUyd_0p5}cTz4<85eTds}JU5=_4)z=De2vGdwNBIQRXL=Z<*B;>_RCG5_hQfFU=zJ=|}&^AFl~<`I4)oo_g_GhgEQ z{2gucM~cpT$zEqZso0shR=t^PdzvRGdi8q-pDB~i(A%%KGk;UtoavY8yotwxxzEI7 zA?myIah}g(!Q8)u+x`6dIWOfsukrg3zah?N6)*Eu#mPKJp2=?npUGf;hu`1kIXl^o zH@05p3es$=y^Fb${cx=+{^_~T)ttrcZKR)!LAGCC z=N!MG&it)Q^LXz2R&h1QIJoj$H?!FrI~gKYnb{3;&I5{U1NJ=*d>B`TvI>^Kyrhr-%Ixo_3~O3}+uBen%e$_A!!u zJn}pGaA6;#*vF&4qYu|#l+Qae-lgu)P+ql^TOH-sj_29e-_6w2o?qDJXs^>xEk9AM zt9PPp?Y?#LXYm97Kla`REUIezA3x{J0K*I~gMcrHIH(zeqF`EBQZs;xBPu4AmBx#j zAc*)@rdESmp@#~@Zr-v&vztRvR$6+q1m7J{QOvyMEi_F`%a=@{F#n%*&OR^otx;3boU2coONIH6z^H| zB(E)+%j=5f@LomJc<-XAyiZYKM&{;Rp4(DQ(9CVwFdZ2RAbqn6^d@^fyJE@o$(wtD)|3TPHoJq)PxGJH%z;M9 z{M5~%pm7@eysnv_w)ym!*anQb3(s(-2D)x=Zs*ceTyj|2UW>+Tw`f8uEz8pOTa4xd z7Gvl^i#Gj`#bVxWv4rlhh^f0RS>|#}R%nGqljm7Vdwg?P@1BsSD?QuOB%STIEsn={ zZz$w69`YIoxs8SVn$dsXE%kq>fAhz*FYvs%PkntMWLm|eAEbhl)|R^vTSM)|j>7nlxRwb~SJaryG(WJ-3AF z!uuT0mG8~CUo=6wACIu%c!v}gw220V)qyVE_XS-VQ&|w_A&e8l8Qne(bajmG(?}16 z(}A`TaUV|e45x7fteAk4lGSuOk`x)@gMEaBpPa)UwtXfhf0_lqulu4;et}IWqH)ouW$^owBzfyi?~=DcXq z{qoGz=zhrCub2XvQ@L6lTO0z1s(rn~ck(r1zPblD=!9$Hq`?De1a&Z`PJ-p_kG<-a)!f_oVAO z(pl)E7j!lPdgz@wBf1}G`5@oE&~IPN1L;9WpP3sS1sZ-N6Ns;dOoSpnpP3fzhWKtM zcgz=+`htcAaV*b3x7X-OEfrp+mJ{9*FD#4~#1nB5@q!00+$3IPoY0hNkoR-oMU`1! zT9D>fDk7YT_bi3Ew^Xtv@`75UH<+wl?|#s8`qzH^v+v(zkT!Pq8|q@exv`5 z7YDu^7r*oSvz5sUR}T2c_MS1nCcdhR{bR>yO}{lyg#D}6Cr>`McEk+v&>geuuTFU( zV^zl^>n2uBn)=y_?A(N;>%VOI_VK`B55M=)n@bCiz0VbFtf;XjNA9$qPe~p7 zoUO|yi+9B71#cJcTYaIV_P$XchX1}{@3}9AEE)Irv`&50&;IkNqrFdL2236w>h;Xz zMNcifoHcV-BtNasU*Ej{q1OZg%&bI>elpKmoWzV)+jL-jX)JG|e^|9r}8@ttqPx3zs!HVyz-5Z z7tbCTANE7Rnq}VGeu~X7|2@KYL-H3Lx!sc|zgxd((b8X^dg||27cPA1#^uZ2N3*iN z{C?)lkBfHgN=}W8OcHorbzs^w+e3Z&Jagu+zq)Mt=9~0U@4x@k`6r)z{S~`?X!pg7 z-(0qDpXrZho_X=YAAbaX^2j6SULHPt-=7yR>da+j=RZAiWLnDFwbzG!^wG~fSFheS zZr!?Tr)JOo+Z*NOkIno1^X(_DUk{EcF8(mFckk)hci;W!vkyFA{{7lDVM$fhn2dn~ zCyssh-7AMqpB~t7>5}H;tXakHA3HWDJt}HkcwpcI8nxQ%)jfM29M-ev)HiqRcs#Om z=dgSJ_O~@!H#gt@ufM*@$HT+3SBDOFcePsI_~GQqnEmI@^&Wlaote*l^2z?6=goV& zgQsWLZcCQDHKA|cxdVfO`rX~NYsB%KoKH4B`slBZm6ZIs?!gDIK6&4Lqdq@+H0_yB zKRxiE(YWy6KmK_C3@?X02wSSBo_xqMZhceKnauvV)($@qd#*dAS-JkyWSm;*kv#Ce%HD@Sx?3>gudr7cPX4DJ?zy z%b$OCd?7Aw*XyH3ee=X!ca8UZ{`r;5hYme>wxL0<76kXdo_j8@Bq{0Q{2@c)?|A#| ze~w(g{>Qar$No0yop=7SnoQ+xue`GSffXytLf?FIYi@LO!lmDTS5Mx$^@pcE`>f)l zZ@--v8yY&9$JA8OX-v0fGoxQvQdi(ni+HwAT|F2R~P7HeOwYB;2 z@m2jgbsAi}bLYe3-+S+J2&4$;(^y z)|fGe-Z_5!x%eJEo=6T4fA+q@!uNIg`AZ6h4YTk0^;cuo=FK%>0Rcna`~LfNT3qGynO| zuP@G@pKrP2j+qnt^_v|X9Ng>SfBfUpoQ)gzytaGy*vh)Po@@U7??rWU=dQlLd-u@4 zjU4&!v@gFr?k$Qh_%2`md~I%S;iCKRkDaw?)6S|Vo+vF$NI2%MQn_8bdX*3M@#(NV zE$yA@g9fF1`0&FGnOnA0cY5K4qQ6&EOt`mOw>$5A>#Z&SI&mWUTz$QNpH-_q?cwKl z*Wt{}FKqw%SFXp)FE6`m<;wRW-gx7)At50>0$+S_#lEv=4KE%zFrsw)_!~)KVe{Yp z;fDpPf`V5bS+l0zbJ;R$khgcIsoS=F`^`^3MJQM`*!SjFUhUq)M%XHnAJ#miU;y@E*TK^n^yP<%qT-j0CRUM zJm&f(mjvi*YHh;Nvvp#Hz++s{Dt-wW=-^5~ye+(m3`THA5WjmH@dGfpBNvJEV|`dF z`th`A46fEi{9a^uf;%q*glg%+fCte;3I`Zt0K_k%&`(;6mR%sjNd1p5Lt}SGpJ^;9_gp&LjdVL(K3wD1JZj|dCPQG%5YK`7Eln~UNW3i4gR2i zlUOCgm;gYcr(v)W9dq(uWU!jSoeXYf@B;=57_>4tm%$7M6Br!AU?hXR7*sP@j{z3F z_dJ8O43;yvk-_y0u4Zr%gSiY&VQ>P2(G2!w(7<2-gIB-|dT$+r#~G|X`7;Jh$>1tzAiamynGm!xIG4d>K+=myK+=Q0GMp3-<-=_v z*|7m*q5(-C2g)!e7?8qumf<8q5}jJ8J3T)x!%5MAL}#E3V{gSSyEfFK4iXK`Vo)44N1;FbE*U_Yej~dkB3a)hc~JXSM1$U(283J%n;$ zr{E!M6gCTL)hc1NPy+aYdV_kSTCG~wd41;(I(rCJovJ&jRV%x$>bknChp;(#Yp{oq z-@Tx_T2&WvKICGET2;`qu;Tg_>wD1);YK0Rj_aN(@Ql_ub)}5L?ZHKUWgSC4 z`4Oia&&Yj=O9~HWkQ+|)9KxH)kAhUzE!;)xd$Eh*{QGbd-IOwEy2+GM9;b%+yc%2byq7RyD9=Es>iPo~VsoR8HV+4E-P zV7*n#2S`@RoVjU8#^j8w6qm%8Id#S(;5?We!M0JTPr(t?f$HFLnRM*s> zp*l=*x4Q0VA`4ISh&1M#Bs%CESQ$zJSl2UW?(EE*Y3!l%x+8@SnkC7_vve}AL|eHh z*xov!3>LVA<&*k*l(#VGGP)+@TD)NZ9?<$9;sG=sm%Ko$AMU8`EJQp?8%3gUtKi2u zNL)SI0RtYAUMJxq2b6Ttd(hqD5ajk9=Dwu)5oPVQ)0*q1b=AsqBoQx>*K0?zxbjja zsu`t=M_9T~&rZvlIv~A8ni$_(@Pq;=<>3lXydmcbPrM@M3U83GuJK{*;3F9x-Ai(< z(mzCg?(+Td3{PbS*D8L3{9G!}w(yDV;2mY6t@z0-zAOGr03J)|B8U#P|MWf-W(1`R zO;`k_A2Jp(5D=oCNc;edvEmOu_f!T=3>p|DB!2F-a(MT021^*UGMLJsi9rK{08)Gp zA(CFrbp+=e+vRFJqjpu%Ydzr|{95uuAu}j4+zcd!))y<~@fn_xUoT-ft_SloK1t_3 z5ngdoEI#pxoCz1Q}Kk=8`Yq*G>B3~OA zUXh1i;m5DZjdb!q0*oWR(g~mEd-JWtv^ngkv6~00MgoHm)OjlOf>aueXS#1 zRza}u?b-;?YT2}7Y_#KZr?y=6?1+{z*(v?d9<=#(g*f^PIc)}DxpIL;biA(3h)-nZ zq-D2>QF81u1eY=!J&c(%}31~U? z4=4;>M3>qv_DbB{hP&PVff8PUtdGF6K7sOPR=>8$E8&!S*A-sR2)M#4^{Okpp`G|) z?ckMq*%kfhcH*1b!6&qXr+Ob{8DT(S;`d)0U%v)oj_qm@`B6HGyq?Ed(+MDK@-&K1i z_gqt03(!s|gWnJM$qRRv!~9P6XE#PI>$MWW2qh4OoC#N zMTOdtn~h^PFaf1{nw;WS+7YU|$=zx@VsfC@qVl`Vc0{3LklqeG!Z4E3k`Ks|+Jbz0 z->s*zbSe5C&FB>Z5+SrJ8SGUOXs=x=K|JDhEgIu?Vg3_$V}}~wrh8+J zlMmK3`C`qJ9&32~utu+g?MHl*&^N$jJD=pX=!v#-$b@_bN@ymV9zg*77 zx}9Y_&V^3fSA2Rh*HDUcgCED59=l3spJ=uhzoGIHGjX>2z1Q~@_lY;#F7@}YX?w-m ze(ugSOh-6c1N8Hfmk!&fy?c4N-~P*`slQ#;ZHUJi^f+Id*Vrw2_Tv8PSlfwdT!Z%q zCfhH*T!YWXSlh3ixQ1V`MyLVncrJ|N8Y+?goU+#r_siIJIcLy!mvb)uc==`C$DSS> zXFG{=-hCs@w%To+O+O^gb`fX1pThd}VVtj6#d(RjXI36g`um2<8>+v)d@=jlAN=onw6>1Rr~vxw81>zNUt|`Y%0PGjr|byWaWNW&cIzE+63i zakjO)=*=U}_5GmhWzXsyZ<{i4sO`z5VYWHR!)^4Qp5MQ4_?;i$za018_m_XPUAer2 zSBW#}eDJh`#pjWa0^V1oJRdM0z?t7N?(Fr0#RpY7v0kMs9RvKYga@s?;JcQ1oMtaR z2tNEYoNGA8YsDPkzeoIoDvekNywCN$;DK5^g|(OKr8vP-nT!Sf2tm$c9eAWc9wmC< z(L}7DdPybcMK5#|asImKhx6Q{79T1$2wg=_@Q-*~DCkAK;0c_cJt~%PoP@g%y!{Pp zK9}+SqLB|0`{IoHg1oBYJe*72gA209rfx5`Azn}TGdVB2(Y&uXl;_31oM11&dFWY! z4rf<;iINOKeoQ=Y7O5od2#d?=99MucL{l*AEm|LpC-buST3VZ_L|Q z+#me@g~}9iumO4f3G45Ef0)DB_TI95A>B%tk5TR~2`X`h%3EB^huCAS+l%A4U~wVW z)xL1@cg6LO(HZES#WKDt@Pa+oM0k~qzwur1Ke-_BTktAY1svi|M47&f^Wwip+DEH8 z0v95V25uM1b_?kCP=(kZZ`@u?XTFDWK4PfAi_$sn(D8JX?O2>mAC-Qn_yX2yo&aw{ zIX?+M88W;GXMKw*KYP~o%HnGyDL|Ag|Z3 z{_!f-CRV6;$dt@83*;gWyq}!9qgdhz_)ECQ!E5aOAja}4dn|aS7XqQvdJ#Q8J7+B~ z_TjpSsDJE}(<+N~g0EC3<${O!j?V5t%ZpJjke-Vl@ox4Dc?XMsMuNeg$Ru3UoLfWkK(RPKbR4FZ&YIBj>SRawg>MJi-=$S4EJ~ z9*{MXk82~57p@cX5`y*Qfl__D1LeGs3z2jgv(mtI1+KZSmh>EY{JW|P^3erltQQZU z+^McU1lo}Tc$!{atU;YfI3J{cCv>?8X(C(*()%0qDU$OSlTZ)o1ewQa;E5#PsoRTx zg3MEWtQQ0cKN);lg#3%B7jnSk3#eZ%Kxd>n3$g{B6GNdhp{W(c=TJ{vKpWd1WlZ?B z;1!iY1Z0WYtcHgv&t0VQrMi~t>*e638Rbjyuf~}g%2B_gha=U+Z-8f1zKJA@sIxV^ z2lC~Gx>aQlwN?}xLAx*7LE<&mdIF~sy(lc=EtArvvftZSQJjlg;Ez$Yr(0+cQB_Y~TX3uyah zs(i&0;E%%N9w=Yp)dj2pz5qQyU2hKspNRiGkaub$PNN={Wwxd6#-eN~Zhap`cOh3T zbQih^xzeDHr!o{!wr%OIE4|gD-$CtZs6*L~BH2sZU+j&#=qK#ugFTI~ZXEmpuSj>5 zvONiT9RpeYPxP1Mv>bJjRDRIETh-mGsKYeiBb6WNr=q)PTO}Q!zC#lZx=UqCxcecC zXH@>sP1m|hx=pg)mhKXdsouDNdc&3eUb~CRzZ39$oBB)jH%1od{+9*I6OwFyz^7p9|M zLv^%V7kQ(O<)m_2qVhpMLnZaq%8|zsv||zuzGaqm342j(0G{$pdQN&g1Z`p>7cA-V zX?#ac?Glx91==M^k5Q)S==Tu6u6{Gzb`W@y{ZQmv(q-Ud!E4gx5~&Uj0-w+~$*TlE zb?7t7ygq|=s}^}T;UYY>!4|w{B=}7-mXADTpngh68Im3wp?g{Aml3}y&l$Kz0!O^Y znskKol-l2FDZkz=^tcRT3)16Xr1JG{YJV;0`zYmm1v1?qWk~%Lr46Ps#<$aQ8w{DH zF%PxFiO4Ip4N(HZvNERrL<8i2+F{3X{!XQm+Tjx9^C$2-72zDqdLiM_7N3I7QJ$%+ zmA0755`AGwpGgkFfR}KP69q^5d>ZW`l_TkE7Rr%$od#J{%K1ms6}haO2~TY?mGelH zGx3_*CMx4H9{s?){otp!n1(W@w)70uZi8# zm}DRcS6g~aa!2`brOP+d<5WqW+ty=}^=lY&xzgqQo9J;q@)(AzEgh!zPLDnZm91l2 ztd#9fH6v_Fe%sRF-ypX$Z=%Db->&pmDc832_cY{{WZISf?xi}|h5k~0f5#YM0bl~g z*gA~WNQW~ZQv${wbtqdJ<3~a!sP9jGXwu;|&|%`^HPlNz5m$pgus7n89@7|EkGOkL zw$!(qsnSSe9_$Mt>2Lw$=?Y|-;^!e z4r3A;6Mq1`UxfLkDjszsWY`D(RE&9PoOKHN_d3QJkD{Ck&`+WEjOr1+;4Af`{{SvX zApHoE8~GnN#j~-EBhfI2Gk{G(7!d%-#(~gBG6|i{q<1=Bd!3R`AFkw z%!6S(u9f1RMf=kaW4<1Ui*cYmSmJfC7>4;N8dLYfI2U~naTfZXG~bemJ_elwq)OXc zoPju0zF|U;#AhSu<{(WpcR}r}9(89F+F6QcLA(N#FO?C=av{n)3gZl#r)k20SGAJ< zsl_~$3Dp(UzYwXtMVmmfmC*KhV3GO^p`1LHp)tt;@RH_{ElNH@w}4_%2xnyCz_A2J#9GBg*W$6ONj2a(c8 z^8l6T`_LRIeK$vQG*tEnkbX+jIq;cq9>C?JJ$(i9I2X(Z@qW@%xlN${A(aR9o#Z-K zkjoWio(x%!5qph55a!3!-UJibpGVMVc)N&e-cB;ER##om6D=%Vx zCYJNC(;VJpj3+2RXTYxl-b0c{rJW^tbL1_JQ;0?%$O)A@#icPC<|L$g$B6tWWp=u& zsUZt-#jd>yWim56Jxls#~Q#J@pS9 z%Jh`4sX@)F?aQIVr#wsz%fdJt=-JQeObwpUzvUO!+DL*MdjC%Ugf9AA5WFTqylk3RVIN;~>1_Om?+w;wnya4H+Df!M?L zLD&Pv!Y+^m`@k632}a{v=utM<4dk_Joa~q#MKR;Br_FZGo9*|g*kG{V;|8If{T_)h z>>bknim<0h`zzw5K0Wc=2MEP=BR>Ktw6Tquw0=pk&rncWr`zPPs#aQMC+%lO1~A&T z*~Q`wM1tL|?R}!LO0=zgqVE}Wcia0!D|WY*=C)S$Hd>RIK?WoPF1RkZ6kD3~!0B&; zUVe~Iao`lcVsBH{INqwgZ3A0`N%V+hG#0ZxMTA?~+p;s~PMw{W9d$QhVb5#1ud_nO zz<3{xbUNCuXeFnkYi}KDHH)CE>zY6dI2|0ze$x|LhkDcf^zhvbIg|8{b)%ryN~jr* zKzr+2lNh~JmfvHH-bE}%)yZ^PB+->UP+8ZS070%rJc>l7#xVT#r+e(HM{8z@reb>x z#ywqHT(r*J6W8rJtV_Tiaa&2OY0*aM z&{kou#)a662KyIMS~b{{g|78Wy4EZC9^u0!_weC1X0Wo_=glQcdzeWwtt~st%Bv3F z!ILZ-L$M#m5Nk|}atkKEP^Lw>nUJq~*oHaZmxs`vOje&4hGa7oAKeZy<7oEVKBPX$E zhYHWAd^0{VHFPmpAG}AUsoW-XI=2t|qgc`BJApC&Nw#kXh0$PcFXI!gA+txk4RZk> zYX;@Np4mfftHr$H3AXnjhdm^%?$C4WA!&7otYVKzt2+m12IR|RI>Mo&u;&pi?A3S9 z+=#toATQ1D3@laBMf188j?&Gu^sZm>7=6yyw6Ep^yyhIf>%mf9>VJ$vnI@uaN1}`^ z=zomBK8nqIQxe^i?%c+<_oWlCUptRIS-8@UR=!BH!PL+heCY(fJjeJ_F7YL>i7)w# zFZoF0G6%jallT$qt2h(v3S17Dio zDevhhhxM1jIqz|%$ozjI=Qdw1d9iP^oEzaMIVbr>n@~vdJq3Dhg`S%slcf73>x8=l z&)492ES{78<1@Vg6YiVvr~@bt)dA^RZCSe36PKne{eEjqy4D@{Zn)HC=^^A7%BDZz zRun7giGQge>4|^oVi$UnDad-#6Z9u9whnQw<1>A@hE(wQq)vrPz(x1}s}s`qC(sAd z2|9Da@tle4J0Slk*XH`LTYZ93|KXenk_$~3>PeJ4WUj;dC6ij~q=xo}Z9aQIId9-9 z>YppnGpY-^y9t+iVt-cWIwfr}kGe{hcM1oa?4fk%MT%Ouv=;49D)4EGRYgiYF}-)Z z4RaXp8wV|WeR^-TEpzSA!I>O45c`C!$m~b*0T|9T%tJg+=yWKrsVspmQ+&DplIxsQ zcN6w$<;qgs&47>-?3sE(syF4mRVgfwZ+7H(fu%fX(LL#1TRhqmDNR#{qLD z!==V0w<&G)IY6s{(n)Q8?ULp`N9~g4K1c16)_smTtkHVajoa9OwA{0VFRJ_0)DQ%o zPLSH7&Qe=c+te1(JUh3N!jPS-?o+PeY|rMl=xk4Fi>Q5*_nI+319CH483Y@O6;QSW-<(%|yI<&Mh@ml~H`H{VRQsK0j- zvgO?0tHXLd($)W!Be@=;dflPVTZl5HW#xsTfEb$=P6mwaE!SK3y=Pzg{D;%N=u^M) zui3xl47qf7x0HK-AM@^^#Xprj_n^<%?%DVEDadepBkN1^pYQ)`Nq1MzpALQ2zsvsG z$DHXo`~P2~ugvnu9}YQAzD(xEMcj`Mg|esS7>2|cViV0HhYpBMiA@|6H!}8-9+(xD2ENvezm;?UzBWGvc?kpGq{F^hj`lw|dL* zQCE>esFp6D;lt#|t=|6u@R;j}SOOySQx0eVy<9+gf3^%K4Fsh0MFLX#Xx|mW*UNCyMLQs%QT!u-COg(2qb36FeVjH3;#qJ#v}uJ z!k;9=m~vck8g#*n^(Rf+W7FJO!pAhMoF8c5HtK;P;4c|dwz zE5n#-K%%z}5Lr$vkzvd#Kx8Fxr3_GF<*u;z;U=uBsB(%G2stCcLRguA%Crm zcQ0qKgh4BVsSKJJG%yGt#rF_!0)|kcTBqWLm8uezfd5sv&sVL)eZC57-Gn;zd2DmC z)verZryG3k=iPbXxO<(ufPeHWg=+WXxUY6U;*PCOH0L!JHP{qIw_f*wP7vz78oku2 zMjxy1QeU;I%D3A02wP4gcnVCT{ZD8gC~~{Vk1)!-JJq|6{@)nB9KucROXja;eiI8{%lzjU{vz{l zVt#202E>nM;c0&f%##(mfj!9{M61!b{gjYj^UG-|6S%!WqxYckY9CEJHX=S zGJFjSZ)JWu*NW&DGQWX^FJb;lhTp*auQGo*^WQ)o=)3o7=7%mx{_mOpPYMtGdFFqG z`IYd?$nQ`dI6H&$fubmOfgJeB4yCl47eSx=N;^x%On#+b0Ns}SCCCT4H*pbOX}8HH zLw?15$^1&cVLbC&S^52o`IUBg2;x$FrJpbycn5M^XLua_q7(ixCwy-wzsV^)(Q>30 z>EusziqAEjJFE2M=&N}8vYunM-VBPhVCw9d2qXh+t21W~Z8kG| zF1|fiXys;NN1ODibLOUG&dAN4I%8@MzKzeE=@3GCTW%T-E`{l*)fPAk9VJb40PNhR zj}e>SBjH`(Ts)RLtl4-!Z1|bu5hH zN1IITGPSLeyH$HGwa4U$9+56b9fkfNHQH7JifhmQs?Zt9BFt41!u%}R3b}UerA;<% zmcDY(Ba#VV82T&ZT96!o2V@5&9-wB$g%2g+`r$%9R;m{XPmu_gfDS+jd4Z_A}@CkK-_3w2k90X#g?LUfMI5>+miv*;b=2Jq3FWh+}AA?Lg!E z|1_Qk@l)`g^)%xMfG_~L|6fVOk5wI2@vD(h%i7X zL4+3|#=j9;@p9Bd5$gdVkP%Y=F;0)b7)Tm#)&e4^dpUz83|bjXWzfW+fk6N%o`=vV zSXCav6`>v*9-bF2GEVLQQOADt>#|?1@Fj@+2>Tfzxq-OIuhf4egydh#@FMdo_~*$F zMk)2%2*js;Rw@gh3Fyo2iGDpU@+;*E-bj8$j^1W|MQ#o-zaod2=as?}pUK^a%Yi&c z<>%H zyWRD1g9k>++fo>u_BUd0civd>f7+k2N~x6lKXj}3l|BIVImq2=f2y+_ys1A$^C}XJ z+w4y%bSNE?106@_yAIJIw-dl6Zeu`2v(Y$BkoQ$+VQ3IMuP@3Q)x zga@ScPn4Gklk9glFbF_+cMrjfqHrm=9NQDLaZ>xBlrQXYE&LCVAB-{plB01y)&ENR zaVz-=M=lH(;g$3cg`dJSIi)Hf#gX^lX{s+BRH~_xm4Y>Ihem^`0WFb?kyB46NU!~ zfR1`dVU&x){6|jNw-kMAw$4N%!{n35w{YyaBEPNh282eEF_kPs6QHBM+)IAMQut(m z-?4v$xGnUfm8?RJXnMwZXF~@uQ?#PR*1)&hZVkF8nl)Tb?Uv(&A`byV2HmKfztv>r4k{ zr)}5Cbixpy;)Wyc^66pG8amS)@5w{_YdyK5lem5q)Y6)-ddxj2>q&7&viO6*U!wUD z$1o?@Idhu?zUZjHANt`h?ve(yuutvAA40fGHl!s(2&|+u(|ZK0MZ>(r;eCRpQj7dp z@O(Yi73q+kk;sdY<)s$s;CspQ0d1`_P17ptOdm!$JcRNXk8(-IH!kCBC@1L~mk}4A zSz+Kz*R9jT8XpM7dE;15Yfzi6Yw@j>$&I`IfSJ2g0^S=mrt2n8_|v#c2J(BcFy43u z-dT}ngC1f0;9tXCg4`t=0$7+pJgeX?r6Qk(&Ir>5eqK12`p1prVw_Fh9r0}lPh|-C z4cM!FX2n^|&-@I&nVvT_T3_HAjiGY+{UBid?6n)o6^LhZU$_EkEn5!WAgyImI_Mp0 zbD5lezHFP|c`o(xjpS|a&s;WLx{-WdBr6oxdhtf`BD@Q#va&wqOYl%WA3BUPZP9p^KbpXCTd>AozE|U3!|pY>_hR>6 zxYx6LJ?;b8eE{w|v-{4t4`%nlxHqtS1MYjV`(C&YWA|aW@5}D{;y#kyN8)}UyB~=A zXm%fs`yuRp2)Bg;-%{0FSptVOhR|nxXH}$iw>~%{GS>KhmzVsrBKBLdrtY`oM#mX_ zg$QjC@2%}G_=fxO6E}zQk8h3=cpdz@UwI!LzLnFZN1Kf~xL?Zm)8-(*Y4Ijwe@&kM zg+UdT3y)M-hH`%4?{d6$80V*bmvaxlz`N=2eY0*R@><#DWxVeI-gm(AF7KmV$Em~J z%o8{FHb1_(kkjk*f*US(Tsj@hlDf5gA8i)+d&=__|D^*DT9!^YXt_h+!%y>W+TH>$ z(FYA3#mxm@r*skhm-el+ES*wm*~RO$YdIm@Ef4hb9^Z^_W_1O;J1#-Dm{;o(IX~S_ zzONQG>@9xULjS0{t1VHFR$ET?=`1W46xJ!ZmERP zmC_%|x#>bVL6-)3S&QA%nEbl4|Mmk#E_>c0H_|M8$E;B)w4&GxO1o%@4`ZE8e^yh(oo=jgZ12y)04|Hr|9(-xQf2VZZ6|HOOJf!6#7eK(nYoBRh) zZ-xIP|5OI8`49S@GW`PRVgbsx1^;hR{z>-R=0E6ro3`wN9Jk5;TaO0&PIW;&(iio}l!K^$ zeC7H9vfn#UrZ4j!vJ4)P{BL0Lzti(&&_4kB2PFC5MfDGpf1#+Bh z&M&CYpX#cpeyFFOsJ7tSSS`LO5BG)aM|Lx9@kJet{i$^CqprnXFuEBy>~aS9`I2XW zf7zfaOW7k;mNA@9IQAFNj^=!{1-wuA2fUXK-wW%qP)4Nz+(skLQ`SX2XPhxq^e-D# zY5CAN(&A^HZ}bi+^e-DxZTZMM$s!C|WW@K)sE7P?MMDex51<_SkFK)tU7j{Z1?Tyn zGA3I3TPiH4z0DTCm}iXmei!w*TBk-m>f^^5tB1bquOEHDq7NT!*%kbP|GNphE$?~_ zvta)v)PruiJHYqC_@xN5)8Y{xZ}A_yz?d6S?60@fSOR^R{KpE$X~T*UwgzFxS-uJR8^Rq$xQ8s8P!9MO2A7BK z#bGNE<_N+hTOJNxj4;~~Cf0&)h;&BHH)75QHnb;xqTIM(8a@{O`Uu6&X#)ZRx40aBa)F=$1N-fxKrA7z=qO95n87v*8Jn7Kpvj=SjC^&6cuy-Y@Yd#=McWJ?D6 zUg)puG|+(z^u0X8%tpx%y~{xVD}iv(@eK68Ji}sT9OOR(eJ}|JJ%H*R7t$ zH5%&2g{sD3%rOhL%H+)0Fj4DaeK3~SjKt8aFcdaE1NxuTzl0qmfW|#NfSb3LD=I-) zB@E%~*w{mb^W7c419=YLjL>%xsf(8mNnN}I*Ww`=9gq(1@^3Q+a19hM4fJSCqQoV7 z8G_s&%omVncXscNdo8=y;@*ed`{2FmG1!l5f;Jiv1e&LbCa9yz}0X~FN1`Wx8C zR(>Xb(_4#gV`^10Zwa1Y-$1Ni@_4K7e=3^ZpMdudx8Z$F@1`};wCukc0A)Xe(t2NX8-l^_ty8^kH0bZ(i;^OnQxqWVfa@UcE4HSTeZfk*UY|WBTARf zwH&u?nD}je!;=q=y?@n=-BIiJ-l^KK=eb9={idtkJ-q0>ug6ZdyH7J31D@A@erI%O z*M5yN`i}DVjvkjhM?3Wk?er)6?c1LBeBaqC*3@lVJMxL3^||86?eC1u-qWRS#i3sA ze?R*CAfGF_CDZjCE7o<iOzcP2` z%-_3IBkU>oRAjrKhYzy<=hFyZ)6k6^Pxgs=$G3Qz0bFjo?>7Z9Yfcgk?mRzS#G z#0EgTI-&#+@e^0eFs1;I!sp9yQZ69TNnp?8*>gI-62FK@26tlp8nP5oz~Ed!{33Gk zhr}gj%P=OHJx^rM6WDWF$4&3;EW=5(&enisZv^p6tY&_K_{Cho%P3u|k$8%?N`{jP z0V!Pr0VzEJ3=RRGC|tA*ClSMdN(3MnW0j$MDuX5l4GaQMIM@?7tWAv`V#2w6s?}J) z>LFBn9PxlJXmwBtmWA#Os>1*Ipo>9j)z09(!BxS4-S&2?>gFM=?pD%Gt;+9K(5(<_ z!zFhO|Bmylx#UN}mH9iGMX9Pb8Q6I_$|^Wzh2XdZGff^R z&rZvk^Tf>AZ7%d{gBV+f*9L|R#QhvznVFU?PffQuX|334+gOb^a&`{Z@&5Dj)^5;FY0e4Ez%2|{@Sz87+5|_SUj@J5WQotr~D`O8CvA!XrD=fpX@Ux znpH;L(e}2o-BMjc&k5eH>%K^BbLcbLiu_fEKozaI#|1RF-UwVc_WK$M`$iwwH(+mRvei6rf|qPH&UDB|LUf3R5?7aLB-;pV zLV{$gxr*@)jd^wmT4}s=O|sR<>qD;y0pd16XtLEPVH8`=NvBnXGL{e^X|umuFd zR|SSKt08|s__r-aT0G>mjDp{CYJp@M>EUHA!#+MD1h-7^ z=F3uGcj2&yNs8u~%eB9eJw#h-fek3E$sXbFyU z{Z!CTm+14C$|W02Y*`PlxUxR5ucW#Oz^TgK7PRM5@y@rku*JY8^9$a+yu&jq`e0AG z4|RT_{E6Vj2z(n0Y`SURAWEo?$02SzWhDEv@Ly#-@$%lxo)ri!*k5e0zbuFSWjX9ErGET` z`cQsiJ?3LX`04x-X|F=qUu>|yh_Jsb$DC#Z>@T+dx!8YlS%v+=n-zB0UrJ$rSq}Tl za?Dv{Z)`X0@23+nPufs9_yFE_0Pj0shy7(a=I2Y@@+Q>x&YM^d8;Xdzd|b4bx(@r~ z3pj71&vMvb8cxkRSiCg@THm{3G?f`APM(XDRG2xC9aQ7ZLUs5%!nmnA2?Vn_XQTwY=IM^;WeV_LoxF zUzWrEBGHHaMWnrt7Z0u~#vF7*Y(kYic3qVn_7|3Z(9fGxkGW?N_7_}gq+1YSe-UAS zSv~{%6@|_|R2-Ur$R4`-kRA4yQrKUX!~P=C$DAYfa}-3(JvXdHdBi5{2mSqa*k5G& z{G&+!XHvcCg z{iX(&{0Coeh5y8R(t+0e2Yol0ew+LUPj7|)B>z+ft@%Hx9`+Y0{jk44{+;-Li}Fvh z-!}h2-`msx`-|8n|8G(LiT9)ft>ypmde~ni`ilJ19=&va1ne)%iKo3#|18a~K>bjG z`k|%#!-gTg485Vc>eOJ=BMGQS)*VFs<15z>kbT%+B>FP{A@TITzbuFSWjSbJ1%x9PS zLiS;Q!5J~q`5+?fFVJB>5%w1m_Lqj2W>poJEvvGZy;)_4{iPK4m*t~5pXIQ>l*0ZZ z?bn)xGLr2t0@+_|Ge%Vwm;GL8|FFwQJM1quZ};lrve0V#N8OU_u)o-7e_7aH#G+AE zI5P|TpXK;h-=o5Qx|`V!`-_eCYNqofV1E%|f3a1MI#8^?dcdv^ z8g1XD-d+5zVYmI=;9+*yUr-OaiLk$jg^4>6W~bdFDBceHi!Il^rdZ#j#vT|l&JO#F z4fYoi_7_|pBJ3}=_Y!v%pSiNjex~zK`>XCpiZ}H*V&4>!Y=`|tg#87VhqxzkH^S^j zm|=FVde_ed7$Jh32A>-|^zlgBE;PMb*f3Zy)U4yVS2s_UHjr&oAJBn}**NS2$O7oSdFu1dTd9SSUc_E2K$Q$`-=$si|w<~Ul$*}`nCON(0Kdj zZu=2#Kf;Z&!~P=44eT%Csl+|SORwy)FYP?sez*If;-ftd*^h>dwZr~`_5^wbd4c`K z7N58s;kF}Oto^jxLGTW896ZKOd+);jf=ex~8?_%{_S;|TJj&h+<+0QNy$CkjVSjH)b5NwQOW5BrPM=6?)YQCIidBZ5ZRXSv}V zhaQy(Gty4`bJJN&u)m0~zt}F0Li=#_pxrlUjJ?pU3T0Jg*M}t9VSj-vdWai9=a3uP zev*|Q2kgIg8*PXEMQV$EK!1OiQFhp0MA%PJ&e-rzQ zJf2Y2bxF1t!L2M+P~%dSQJ*vedwh}o#Yi@o-d;Az1_PS14MvVn@5~TnI}L0w4H@q2 z-W~T^cCW>~54*?SaEe_gzXR?&vgaLfAH?p1a38|%L$KEn#y>n8cStrE%%RYGZ)Ss$ z#~`ubvl(_6+3rPgZm86-!T5mpzF5zvYTE|$T+6X1*t{|nLCv1It8Dc8cnvHlN)b*P4=Cc%NAZH8_&mo>v$j8 zc?NaZa|hXaR`$+3NcNt?mP>Du&F9BE#zd3dXVcdOBH4bXuKwUDvj5Bz`aD85pe3jN zkwbQ%BY&E|BwNs?aehn49`ybXPy9+YA^m`j5o8y7AoR~wWE(o=bKxtp4{d*CeGJ)% z?s!jIOm?D~i&eE`E6UCB(UQHWvH#&=WHai$ulGc<8~qZ#^HH)L{qX6lZ<77!`^g`P zWJ6m2$x}nfj`Y)qzYHf^(tU##2ar8!it17#*_8Om*sWw&DtfKHnrusvUnRSfeW~+< z7mkvRNpG7vp6pEFhFK4jt!eY2F9wjkX>ax6&17>L_r7^7*`5A=?&TlI_Ow3XjT2;l zn*8X#U1Wo*TyFl6>`-%loct8oqTbp3$x*ULHGXno64|6u`*(^XyVUIWFRmfm)cL_v zg2+A<7Baaf*{IHbx%hjsQw2V~`xCNN^%*z&N3vI~PV4>if&1^Ydu%Tc-Lv_HLm}sC z_aA-c+n?tj%K7!VG0*(|&zYTOmrlFr{*P6A*Y0{|vi3vohOgAC9`NfrJlKDbZdTW& z&!~I;{?vD2#l7~J7QSyEu=T5bf!eR$8j|R(U%hi$x46$@@2~vo$gafjXQwur z`9F@{`~J+zlui?dTMD|wXIn{#|1WU89-aj=sY~&{pv$u=%>B2!kN4@W+71+*ia#K(aHDeGPEZdO)-}5oBK@d)rb##80%! zFox`F6h2#qlac|6PG9ysj6F91;ujIX;Ck4}z=R00s}WS}YDw@Taf!^n7Bdij{380Y z=V3C8A$t$Kw_b*mHX_p|*xLx=mq`2Dke?uaF>HUEi1kQ3#ak!CNo0ScbeRAt9R>yw zR|?1MZj-2Y(Q}0rs{H-8OdH411fk+fvxu&Iex%zM|N#T-n=dkqL4GaFM-Dk(Vm+BVz`( zf6HO!FY&_-oMLZN;*)(%`fiBL$Nn3BvZpEXquARNd2?lN>qW%j;&CB*ll!mP+i0F$ z`f4+!4SO3QZ^zz7;oG*iDX$@Wn{w~k-bNvA$=>$=+xJj+4T_AGKYVoheuUW(GjpYz zCX<`9y^ZGD_fmlwV%8#4=Ge{xe>R1@X|DW0?OpFfEgV=IO_@YOW{Z`*Y zO-Y-ZCcRqvOG$}0C!3r73LVm0a$#~kz(pcq>=iPC{kLbINMMl+phGT_(TfH>;t?Dr zx5<$n(sv=m2dRc}+DC#aRTD52iNcxS4+Ts@C2In0vO|#lf*i4!E_x5y-5JYqcNccmKKaX3NtVUKsr}i@d>{2 zTz*FDqRi+EZYb1FK7OZ%)f#8*D-$y^RHz7Lgrt}{8r{)1lS$v zoXcP~gHsp;p!E7srxNdXuZ08G6)ghY`WNO%W-Kl0;J+S5_MN&CyO z@Lk|{T)#!-M}8_Va)WV^-%1`hKird_)hFC}7C%+Oa@O86;`;+}Hy zcLP5h@%^3fO)JS*M>8e&iTNp0bEHmZD_qVL*ErX4T#}<#O_}QYRjzQZp65()NseBH zU0viAMatF)N|q^GE|D$~c-u{I^HZ?TRPMaFuHM)VdOpTVH;a8!?EI9uPfbnBa{20; zV2Mv{$74^~;BEH2oVl4Zrlw#ORZ80I8S-F-HY1t+bne`lDGFy@@3*6bm^1T<)|)LV z$R^6IwqtaDk6gUi@T*OkMLD&Fa`9L$SSbRQfKHh?L&|yzwgzuwiRJCCM;m!IJudk%>9{RgY6(EfCTzliuYDUk%%4z~j-!Y_*=49Yl3CW)J#-j#w zdTLoD6oDiUcK{^yz}juR!*M)Hd&$s6ucom%dmrv@-O>3&j%RDNZBGiRCtC{baR|yQ z^Lv|YKYwJhoqWY?GvVyXG~Uxr&riM*Ym;FdgYgUwWpEgS!x>Cqa0G)E32GWsYmB&3 zdvM0I-n_A`Z&ub|U6XQ#l;gQkH%vr^8N-?7B_aN z@XeMNa5y_{A?#N;W9tgak`4gP{Bb@w}7XD@)^N=?I^5%-iqrt~funQ*QoYdyC z%5JDUB$=yE=?q*Kgb4z#yTVTBUPOCjwB~cydPmvQ=uCq>eNBV4T}^{^-A#kNRHmo% z^E9OaYdlMVds+iN2ZGPFIJ-@;yW)(vQd-mbpUSP~J?JpxmdXled)t5RpNz5!Kv@N% ztU98sI>}{q=Q!}HBg#tgKj`Ei>*P;z@{e)yk9P8pa`GoS`A5PZfcFPV>1i%2rEX|_ z#vBh@_w|L3p&TAXc|3ye7nxqDJeq8&a+$R*uivq!E9nQF>D0R1f}tO(qBIk;EMtPOLh-21RHXAb?4E7S~FTRr1NM`?uoa;ucy}NaGmYL8SDDGT7__&{Tq&T$$;AMWxQuN)*^&!1WkDNZl=q`lM>J{pAp8; zd2qEV)NxY!<-AydOBJj!R(I6v$Rp|L704LX6MLol-rOWBQaz8ei9>O=@PDJeuY1o` z84J)y74)(7n&A^to!4B(jWZ{}R^D8`jWf5R?B)GRt}j$U7qMPqhX(5*Ej4vyBaY$Kh|MHj;)f+=< zUyOzSUbJg*Wowq)hjz{ce>D7N_y?Djpbeq;dwZL0rTrH+$-!FR1x1erNK4`j~w` zi}fRz8(*&qLLIGc?jOYQMQdW2-aia^KtJK;@}Pm9d;TBS^Hk{hUbNf$@Ex-;Cin1- z=Ts;!&(Z{};rGQFdkwB2L#?!qzQj>({81kuk0(^}dZ4ywX#TpfsqV(x57s@!J+1Mw zKG+|${M_7vUdr5q_kL@DIBPpXeg|1Q+2)xd{J=i}0gegnzUt{8gNHPxS<~ zDJOl$v&D%(=IQ%9_NGUZec=hRJ)KbaB-eRk!ODH&k0(A}L=2ZE*0+KMMr?rt7` zBoo;QtJbAV=;3~PV!^bhbDJj}`7!r0R{l68v&XVu-`zazNEY;Z=yg37{yMk0(84c) zUlQO?v?XktPo4e?UH_>RC*i(V!pfPD8FJ-MJCZ>gdLHjwhRiD~*Qe;b-HF>89nOg& z?~G2!Oh4+}*3}ljZF#IVx#IDsQdY%k1ABekoab*}&$*DqFK75|bxaRgW7CUrLF&gV z%g50!8cW?chIY|t&fU$S&+yJxwfEyc%gZq3Wy(LWy!Oes4fm6=bfGPCp>qs*N9xKU=# zdt}t8R=O0*BTIVu+PCIgZUw7ST=7AS6XBneh zpno(o$*_yfOfu|ZGm{Lv*vuruE;cjCu#3%1GVEeAlMK7q%p}7uHZy4g_5@=N1ofw~ zIPweE*lf5D6dqEzPvIqn=M?@=lQ-VtAMwAr+3L$72<)O()3eT&`{HE0yF(bhpZns9 zL5A2u^v7~j+)ntIdPDe^)E)5$Q+Npf1LjeQb7B<^$M-9v_5cj{n1Vy}la&Fze+2$W zWq+4(M1l~azXN_R#?@;<7x=8g4GOCjRw`VkaG}Byg@p=-gV>(4oS@7zh%zvLCy}c^ zN8uV!o>!(YPvJ;~eHA7uynxX|p4>Ic&gD%^u?r#9) z`&NPy?{faA;6jBZ3JVoZQz-K)MQ*%8x56%0SQWIlKw{Hp{dTrw|Oo?BvIz8jSAbXWJgsQXd~OT5Z?J`zuUg@+W@ zE8L;5R^bMPl?oRs>;m@2A31MQp0630|Dnxz{_CLJm+~Fd=cP-0Y=YL~L*W{QGS6S` zFHu;gQ0fo4|FFV5g(Jbf@_bOjaZWRycgDc{1E9piE9(AMb$U^+;VmgEA-(n%u6r^W89*N;g~ zkSIh%uWynx-iyB&zd^^;E38#msjyI?TcJxKpzAXU-}Z`ahb_v{rMoN2vD5Lo;|&LM z@NF{riCwZ{o{OpF*w)IZHBr@Uq_sD0U)+H>j&bFX*K-`|D#y6m?Q0UM6KwWN@oVHj z*Z9lvQI6X9m*TgATid^<0kNfZXb5@ceC5y=uzLpuJ6xf zvv26}9KSd8*u>g_H@eq%-^+@EH8)q^y#8k1@m(UxI$!H4?pf*miarF2^Kc2@&n0dj zm(XT=zjPNqm)V{;uH?@tdGkD3vo-pR z;wP)t=}XE#v%T{d#dj&bdA{rbMgK(c-HM*B^v(8=dH(Ap#rG-sLPf7t^g=~Hqx8-8 zQkC#2ODYwAxzHr!dPPIFq8}ALW#u8o{|tVbML!h_4ltRxmwh>5zi?GZrqKReY=EcfrENvu2kr(^#P~d&!Jh6`U8kfb&q* z%&xK-6-$CyY9^M!YBh7L)l{*t{%HkFvX23^q~5MhKpm#g zJRx<6(E2WK#`&qnk*CAd(Wd5+r$eB1cfIqJ21c5vG_QEJd5)}Qno`InIvqN z{&;;YM5FOD?Emp1i+fg~Nz-C9cIq;=6P%^V$pyiRzaOUA2vl=ino3h|I7*-iNU z6~7YXv&IxEbSrcz1ay9sqyK;*)cdw-+j``+D_l~qn(m^|y!BRf<0eq%1*`Yb)+t)C zwL(Rk@giP{ybBaJhD&HOUaFuW15Tb}rB4LW{~kmK=3QFK2g>lprAucO%wD`;zLbe! zWsJ#{^Jh@)s7akxAJA$&9o5QY2H%od%giaZ1qHJfFJ7=XtT*Z*rA!aC7V1W+uO1ub zwAkQ7Ag#Jah0uC)LgM85%h{ovG%YUt`aj;+TFizmXZOj6a&h5gKIm}khtu!2{BtQ@ zqe7FG@Yl&ZKOcdY4`pj#1sh3R-8@0QL#c~4v`FWV}&3(=FXiIU`* z5@_rM+HhX>94KY@Brb9C{&LB4F?iQc6;8UV@e-31W{G8=JM<6p!VS=SV)G9rbOTWVzJV zV{Y0u?rpN}?V~ZKX^xfYXY!4SV*Yaya~{~YXL&N`Cq%J#PZV=J9n5VEK1acr|E&8- zyI1D_9aHBf81vtaxt{Xdb`5(8U0FVf=ibL$3-<9D)3T3`O_k}*W1yW3;?3 zwBIjwbpNfEy;J=!%KLTNW>@X`4;a!av3K#_gf?;%@kAZDTrplP2bTrP(^4-pW~ehF>l5Fycxx@o-~E zJRDc&;AzB{F;}MY4?6NabPU>`Tk0*GB>PrQ-6#9%E`+b@U40!QS=AH|_+i#lo47;% z4dEY9l9B4~_Z464+l=Fc=vS){Mba0$y0}REXWQV{w!vQmpY~VQ3Xq||tYR=390C&F zENSn_eJNuEKj1wuK9{|3V7|1ONh4WrDBJ;dgsufee-kKrV!sz$qEPJg@|;wVGAio= z?2|ivMMx`1>#@{!KG5zb`2AQg({G zdHo|g@CRX<#^5}tBRu>(;hC2a)=fE62$wC z4;&v#J&;U%TH8hOLUWn*hLqXT?lHgbr;0YygV>ve|33G{&F2!@Ot0CBuhNZ1S*7zu zR@~EyZzjkeb5Hcm^eF4ng*MaoFBIQQPlusl2VAf{R?>-;Ulc@73ew*d#1~zwepL|v z{{-beKVnHeLkw+IT362%Q(<7sVUT0>!a zYVH>lA)-yTHL!CG<8g!tFl@=XG1oNdUcVjTn%b{ycl6KpYwNo9e_i`^h|!+Xg4?gF zV`;y3qfOMEwqXy}Gsga7+OO();nunx)!UZRj?U+~57QQVh&6>RYin%SoGI>7znb$O zw9#zv?Z~mlW_c8CR~zeWVuQ|7=yoa4e!aBz`rEJU{~u_-c2e!vKC)JUcAF8FO&^%; zSDWpyAD($%e#;uIK>Jl_Yx`B`tF>R1|Kg9$@{J<3UwL=tSdS$>#t<)~iJu(Wuy5=0 zs{`%VYY0==_N&82`_Qu{4;nIj*f1IG8aiMg z_XZ6R#=rpshRF$CgGUU!`Z(A!qvaaT?s`4rUo!twoH_nw(x%;2`nck*)$S_xOmXJ; zS2D)Sbh!~~cfIQPm+23Ef8F`b3R2O*6>MAMUru=;oJ$o3=kQP7Z4w4oWmj$GWn1mw zXVH^7S(LhP9fB5={eS&D12Sa%OP&K;=Kc}G|?BB8_}3%YhteF`Jr0nX69?1@1#|2 zezdc;)36uYxA|8~Thc*WGD^2^*4wlpwAVW}rsXsdCmS-DUm@2hoAY$l4O-rtdo<=? zjG--PoTF^6OZL--V{Pmo^f?t~`;vP{@!ziOvGOd56XTuW5f=fT$PzEDXl$E-Hm~?U zz|d`6?H+**XGEhTXT(0iwa_tOEVQ5pIvz}b7W@fx64)MEum-v#m;x>M40I>3Gqj)= zx+~ZXT5t<=PcRKy@I~m}U>|6~m!WS4Z-Exv4xI`1gBJWH^sV4+(1O2)9s~}C7W^&r zFz|M0!T*5%4tNK&;G59j1xG;(?tvZ+j)4~Z9rT^xUC@I2p(lV7p#|TAz8Aa?TJQ+; z6mTlE;GdxH2OoeI{0sDS@F8fykDwm`3!nv$L(c?@paoAt&j#l}3!a9a3(kWUJPTb8 zE`S#N9C|TW0WEkDdMWrAwBXm!KLD3Q3pPVP4z7X*{iF-kU!%z_V+f&XwfujqM%yrSnB@`|2E$SZoDA+P9pguJ5X8S;vrN60IBo*}R3d4#;8 z=Na;fo=3h9Sr}ViV%LWY{I&ehrEDlX` zBS1A9(odHA;;3N4@|WmbP3uqP&$vX0eywWoMB*P%?Hkn`u`BNaAnRPjMdE+h2LD7G z{4$f%LE0W!J3$#w*Ho|g@CRX<&Yg|pJk}%1KWo-Ke#5gI(9t=C%h3` zAG;S^w9~=;Lxc*mUplL&lTL_V6FzS?L2p6L)6&HVb(f z?=jQ)9~JEe#c>{}&NtIhE%)^CA<}`kd0ax9>G+hQg(pttISSuQ$GM6&^Nq|4626&^ zWq(`#0xo!bNXECU{60Z6->Mb=u^_(aTJ@g~;!h5uZ9%k*pYhk?%;AoTq8VlExA|xp zll)cJ&^jZsb^n_w_GP8B%9j*W%&Mp;T~IDPY^K|r102h^znGcuS78?|p0y;jTP}*a zhv8TYADvlNS`>~`F>6+_-VN7+XBg=lyDpqjYMB-)&zPl8-8b$B4^vq?zO8!c*63*6 zGq-TGf{5cu%UEAgI=^%YQ(^;AWzh`uyv+cWRy5u9&Yw1BXP!QSXywpi5TyFKg!|{NiX|MAS(}B2FUJl}h^s|lbJSBS2 zxfD_#$$N>C$R$Hd88V6aU-ItZ`sMN*@kted{JIY3Qf(P?PO90aF&-FpYu z(ML^PxS;I&0|yTn8vI24+XTJ4$f)}!wu&3!C(A!vBz=h!aYT!_5dH4P{fK;58~na) z@YCDi_gDN#?VI6>AL;$1K9_ob0LbUg75~6&d9Kv&5+Sj|o^*pqiFbw3u zHM3P8E)zH#e%)~XbE;fNxUtKwGvgw)Aau4xSq^ICxbT!V~WDF;{0deUK@^7Ylv z*wOWhJb1l`HPmL%EH!kv`gxfoK$jkjHrMC9t`h6B3QgLqpUr~xTJ7e`jK}KppGDWQ z4z5zMbZE=YZTjO@;gb1jQlD=9mi6f+^jzpmn7YVC#UXX&C2$|lu-2y{BbU^tQhv+t z>vfc8x?MR6dvK-Y9Hl7ES+df0&RKF>_>Q1+mh7CfBz!HnURL$BP1U~|wms|ky(%9= zf^L?Zk@zn4qLKIm+Tgp};7?L~Ro;h$Z(1At!Z!FNZSWVi!7qa^{ra&W-@eRkVEzzA z%lmUbU+NFxyA&;rBcYQOO|sHyK$mxs#`yb()yGi|O%60nPVExqaP>`(ats(UJj&r7 zJ6;}$tLI_X`JDeQG_h^wtJjf{^IpvO_*Z^Qy=l6i@c%+5cPsiP#dj(CE=8N) zua}ZHzsG#VFI4j1SA2<2agh4+B>u#m<&t}*D}k`mVQUq$e7ss^8IXxsCk7<0S~&)! z+NdQ1az;m%a?dzQBV4&&5cynhDueHkj_VXbJ z500>|G7xS(HUc$dxcX7bd(CB}thUxwE;Bl4fpKM%HtR>Jlf+$X{aB_tXVQn0!mp4H zDDC_(UhDEqQwx6>TN&Zmn;no|TK+`u%X>hjmpKX)fZaJvM!umvva%h?>9{?WxR?+G8zeVRSm>*lc!`@C75o<6qHLXB~a}>!l@n zMkl(T_OrM13pQ=%DTlT(qnB;tr;G*mOLY%F)5qP^ueAU0jA(7+glO&jjOOONQ{2n2 z**&L-Q$O-Y*_)D&ys7ba_WreeD;ZCxKFO0d&YSwa2I1#dimvQ?aN#~Xb5V^w--Gw? zZ)|Uz3oo)q`)C_w?U??|_My*wUhQWf51TFM_pQ>ZG_oY`|^SRWNrXwVJ;`UjMK`vsqBp` zb0Xqg?xs`G+D@nJ$6$-wpJq!r?HpLMnRL7}jrgk~Y;mMH#wl;73`>>Y+hVkh2~De> zKJ7@@e}?%2_na>n{#0*w(^Ix2&%3>BBksA$-9q1F8^IoL8#Cgx!>ep@r=S0Z@8O8s zzfaSKt?O;uxSjn&*Q3k+$)1Vq1^Ohv*>BnNWN)o$RE~E3Mb?nLW_Nlj?9TmF$je*} z&aj&zak&MVRiibMyq350uPdH5&#gGdd|lzcX7A*wNOC`2LprHO_Y~h?TPOE$56_4% z?KhnDS{q3xdK#(bp3|1%sm4!6K6Tht=*%~slBrNP39aM>(31SZvB36+y>6I zAzrcd?R=Ey4#U6S%uOF2$MZ&IYv+0Qyi(rjQ})uxu{rm+ct2TrzRUabf@Bror(HCL|^*7;-n77h496$EN)~3{ajTUyWr#0)AZl^ zzwz+J9x3CiG$FF4fzp4HCn21*mFLU-YK0}-r0&le4@!S&tbzF>L9ts70C`lFOYyra zeiSHr@52_hilRBhBtJV%r9J!}7KSP7k#Z&F@d)r}NG4Jyp@>_jyy%=J#GC&m&^YcH%std2zFyCw9~{3*QGVVK80r z_~jB9R{nz!R=PNdfA#*E(POz5v#09Yl$SDm8EE3``c$ z3p>)lC9ITb0UL`vM=mLHom?u4_;u~>QbCw=kH)xy)?;ywRd<=!YTNcaDDBM>ySu3q zw#mnOyLau&?Ci1Gk~tek+Q-Z*tu)$oB{#cwrNwEBin{1+&nchAXp^_wlQ*9ELE6;y z(c0v9`F$&GD*8Ozw$hF`mzmVy?6QX z?tRMrv6}B#dz(*-8I9Z+y9>EHk-IC5 zTnuut$c;ztZsf)zcXt>$CvtJfO+aoUaubl77)CB0xdh~Lk-G=ET;%QvBbSI=JLK*~ z?mp!1Mee>Za!JUwM{W{wlaZT*+~hEF$;fp;ZVGZ!k(+|t)G%@#kxM}?54rClmxtW< z!pNl}cLQ?wBliGu_apZ}7`aZ!bw+L)at|Um4Y>!y$aO*PM&za=_YiW^k$WhNTvz0} zA@?wH`N%ztTz(k2?#T5(?h)h)kb4BVf-rJDkxTO(H2M=A&e}RBd_-Te(5~g38x+?q z`SGPk4%?GGJ8a3GH|@!%dk>oU)lBBXCexQX?`WTSIJ1MtHFx2q=eb;()=)clz@_Bg z+Qu|{^7$P4ub;9fgcI3QeUdkBn_F@zvo!sZvlo0@^7$(GmE@@^&S5$R-=Xr5@Ki+p28I!z53W4|i(h>yobSUAF&P zdW4c>Q(=sZL9pLPPwt&x8Mr6BRWVxA67=%un@RYork1uUu z>D%dgLH8-Zc7`j9^!|8X_pa&~ZAd5Lj(%C@?OOdGzQCVAAB+3q=O5|MN}2PTy|d>d zyYsYzINcXz=mqPT`;mmxNq2aBZR=`=$z4 z*zZEzIq4rc1)Y?gIohIQ^f6tm8PNL?4uy(FMwg9C2Gcpe3%#^n+M*}uN7tG%UEI5z z&CNqxc0>1Wa2q`7ueohcb`^h}UFhS=GhKXl!94e_t-Md%$}UsuWX*!obq;s$YW${o zNTErOM-IAD(SDop3~V{u;wQOx)j}5#p4EiMxv0m~0-JLl?ZmwFdk7Q$rax$)ljl6i zyGfW_9k%FUDpY3#4DI$;G`# zN#{Sc^NtSV+mhFtsC;+6_Q$11(pG478sAj<@$xOlXivy`laA(k6Xx7(6x#}Ymb^kq z`;s5fx8}`S;-;}+bclV*MLgVP(^4bxZRxBB31hpuyx?u!ULkpvyvBK}V6=fZ#A!{B z-YVrn=z5Xr`}uaaYdY`f5pClWDs5I%-h{nBCBprC!{0B#mJ`mOFTy>OI1eW)dXj!bVQD_>IwJ(KfV(|wySy`{D7?fJm;`V zcvaljpZ6V}Hz9%_z18#Jn|^R+V_?0(w8^3btk$$$+{qu;!Radn0 zlxulX=VoNYd)9@ib4UL(zV-ONxLniI?^WY>MrizQrTufY_y&V1qx%DMeNpQ<3gjD2>=pF+#AMRSnRbi~+@+fo!mn0$qF{#8r$3^*?(lJ~`fPGz ztLn^MK#uwRA^tz7`hiXn4x|4pe0W*aU>tTQIo}1$Uj{N4AgfGacTntBa?Xn25`=_b z0>a9g1d82jq(YbC%lRpC{}K-o^ea4|a0e*zvl!yW>KoVJh|W4Zx7z=vD%wofYlJ3Mn)U|W9>+a1-BFI|^E1qJ9s^J2 zYnZOHqRn)ispQRcza82lu9ZSq>Cb}r9|h6>8AKCjt@Ja4{JSTJUJ*pgv*j|~EPhz& z{2+Q>5FN6gmUV``T2`et6EG}R1xsg?l`@7ti@OUSU4pex-Je}rR#q^7*8ByFzi%9b z0BioEi>hyisqD!g^!iYs3M#!oODgqLhc?#6P_k#C8=iaIvUg^+=vQ~CuoXtIA)JAPlEjrZi?2Fo3-mc^c{Lrn~bWHgwOc7S-#&B1Z7Z-Pp5Aw*InTEnznMESp=s3-A7>61*xGHkb6>`E+x2h{{{_!J z$g}4~YllDjFUO+sjM>ZcSJ7wnf8CrnYPYtiIL`gmDaLJ`wk?|{UMW1X%BGz^4L!=1 zFt;DFg&iup7Tm+w`d7_)cYoD9#C-4FjB)Dk{hDo&%;_-S+s1Di2pf+U8Ds2~mzsAr z%3KNj&bZY48tZ*`VdEH)_Nun%dG=FvM)CWwZBdT=cJiCJJNhMlB)0fb_+^V9abM!c zy)AxJeWCo=P5jGm<;U&|t^IIcXz}CJ=gqIlUcUQYh|iF-#`W;?Yt9y=bUTK`&{{*${gRwej9sOSp6>iYm47;e{Fuv|DLCux@+vsFI*GxF*evZnx-#8G`mZ76Ne)KLSp^J6+1epK&$ zYRcBw+Qkx&rhiVpi|!A8K7c<lrJzdo!*S9l32+Vz;x=-3R@Wv&mDjPk)r5Eqak}DDj?llXkw5 z=Tz}s8qsso|Gk)`9lk^uBz(3E*<(3%TM_mr zVo&w#>)pLWwO!jK{yvWl$kaDce}CqC=?ys8ciz`KQTOXh@e8|@>4%Ab zBrcg_6Vb1a;+ZQHrR7QdRWqNxq?5Lcb-RaO!1nWErr2R*?oH~pVwE>*vFDk#@^y%!?)W;x>!fL zn8^2&yy;xI&Fdshw0kw7n~yY+bvbSL5YogJ(!^?>KZ^8liuac}TJqlJUWTjY4jCRU z%+Kp#HpU68^Va$rZLQK`&HNSkQQUWFc7TxIrW;OUfWh2cxTQ~Jl_LHegC%pRcbJ3;<(qYTWJ&6njpJ}@vp5fuG%#HjSA4}jwDCPgn)wCsnCXx2F* zPw*9mD~M0&M=t~s$t^Q5J6GLzgNWviG%&l+z!RVvb{T8`w-^3Nf7fjPey!-G zd{1$YaEZK`u4GV3-jk1zTmR@e^zKB=2<2GJhZj{>`}CtPRpV3 zF5o1-thB6Cw9?YnwbJt2N)HU8KMbPB2GQw3bW49%_3(nbv*wC;dK=|M^Xa>)UaKr3 zA2wvr%u*KK!7Gymc%dCqj@w&MJWCGaE2VQM67-0R7Jk2=sAQg2@aW({1xpr)x*Y6d z_A&J?+N|Ka$|qCS`Y2;gixJRUIW12=CFIuITY_3zKFA1e>p3ijT+12(qDIbN3csH2 z_VD}8jnH?WbK!!{UsC-|=~`#$^N6zOSew1hzkgoKwB8b0V;+j2NlP-6izKbPYwdTY zTKp5;bA9x3K?;#d!zbf^Tm8f?${)8<+JQf04{c%JK$y&+=@NtZf3<#MC3=)k`um97 z252Xke3P-J$l6bo=g1}bBT8Msb-Bw6{+)evZS1ET+SY82bsuha*jvwWadf>@xK7(C z?Z&gz$AT%hgh)Ze{}cE_To6s<|xpfhMUY-U{iCz>|2aRL2K=D8fF z9{sAD)>O?nc?NBdQ}eW4^o>(R&cCNuzy{v9XW&NdDDATE8||^P)X|O1A85QIrm1OE zOw-n$?x&y39Fx&FCZ_4sT`^779T|I?7}NCSeKAd$YCO9$?S)HwI&373q&k;>|I+y7 z!!F5Kwme(LzF(m2{O7LjzxARGf06qar^XNl#-A#UXZq1?`Q2;dQNnZ#K9Rgh!g`e7 z$B+?P!gmroep8COiI0v+xN?fTXEX12ig#yjapoxc9@39IHAmYu^80C1w>X^hj;+$q z5Sg-_IDC@vnao8uc1z4i=(aBI_gyEv8rO|E+mj2>%dl;ko@q-=I6c^wQOemHr$%YJ zz9I~FJCZk^>Z3J%&HSJXqil85o2kEXE>kZ5@CEUR@=BdR_m}1 zKO`Qy@SAX)eqp(mC(oak@cXXwK8x!{oidp|@S>+|>$-kA&%FzehOMK`H-T}a2^mQq z(q8JP-T2+EyJsS8vAOhJ*E1G%Puzj7+v*a#otfj_wS{nJrmWli9&;QDY&V`hN_sfs zNY7mFNKBA$o*`ZiEY*gbNpSBi%a+n{Wsfbi#{9082QR}-5MR$n}3Y_Jo*Fk2+zEguXvsG^PN=__b0^te)D?5 z|BQrxWu5oa%y>`9^Ksss45z2j(Jk|fOsD6|%p}hxN7AAK;nw`Xk?v*q@mILmj24`88@3SujJC*wz!77=e5jPkvT0#34cBFwce8XT4`@( zf86J->`!iaEBj=>x3WX#ZKXYteYDRL*~e~qBKvs1C$h~k%@enwHyFL!(Yqsz-fid& zM(=j??hrj=?DNDZ^v0lf7kU%J=#4^e40?B=H&OJAG13$Fp*I!12he*cjNX0dO-1hk z^d1sDV=VPV0eVH~%|UNo7`+1YiqM;b-aOGW##~Phpf5qihb zJBi-uFnS-McO1Qw=$#flW6b)*S@b?f?;?6%htWHW-sk9DMDJ_SGsd=0G^6L(ll@8T zp6rtedqVR=GkT8I*~em6XCF^kjh->x^EvFnaA*XCF;LuQPhxR%e&2 zT6(0BbyW22c1bzN`j-SJd1}}iQfL>?yO6+CpJ7)b)e>qBPla0dTd=SymygVKUDMn*cRVle|*)Eb=>pKc(=yy zNXRT=4a!;Q0GZX&2XuG#}yD@c1_N0cR4tr+b8*=LI)S^6V;1$}mhBaMm-d;D}Rkv2N zHSEqgSaY)ScQxNQVlxLnIIb?qU0>tZQalxe>xd)8TWGg89AlnX&qD4ydS?FFVffRZ zHD_9Jl=lSh^=H;?3_(6ti*1-ZAl`e%{kxiDj>OEhtQ``6))l9E$97Ng9&;&(AyRE^7{bos(#CdAwzu1lE&4`Ke7SD?DdU*eNe2-7~jyGx14RdUr zJV*3*N%79%IlXSw>UQ(&PxvNNIaWTEd$sPjYu?UH_5MOjYB*SRu%?%LH~LALM~Ksb z=pN4P;O)&f_=N92y6~+U)~RLYsW^17j&atMXfJK3%pvOgia&c2W4#CYuDwRY)E&jI z;we$ygUY{8NKYq8f0D))YRL^}d&GFp*b*DolScb=h_1VJcn9x!zFn0KTR1YAG4Z@t zLc=|_b`6q7&!hi5Y4k0dy+QoAEvCKqrfgeX@;8TSbboRV)bzK-vW9a%X*Mx)z4B)= z-{I4N@!nfAqUx}zH}o6lsGD%5p(ZoOUgxyg^?94me~iu9%u_=r)tx3!_1E)MqGvtN zT0bJe+pj98$zw}sSjac%XUl2&E8nSu-S|fP$0c}KpVe^hjE0&|dGC$r_*Hl%tOu~O zo{iSLt{>(!`N;>i%K94jzz*Kb=$xiA#Nj~Ac<1%w;=S?2&5sG^tGx3$M~BRNatJHy zsl=c5neDxw+H4IIxVI4c39er4rq!LmkB@oY%{|7~4OVG!Z*G)#J8RBv-ksC*Bhvai zmG9IH&dI?x8c+I)$^5gK*Jh`9XOO<`CyXNscT-;6nAuC^wH^JFy;ads-hSwp35_@$=emS4h?IZ(cXdMa+;2j zra#tEyE=j8i7Z`+zAJ~j~!*9}@-Epbr9^EJvfSCy^l1o>wi<>*QBla!@ZsWxw>7T++T zs-Xt!ji;FS{FrYul`>Y!iygVi-aUz_UORbE^54hk--&*o99xsys81Xzd&P zT9h~Qhqk8U_%T8G;TmUanwy^NJx7}CV^7la^?;;gZ#&*o>IcbtlIJQ(Cz&=|lax1< z?cQ=5Wx|YuH515d=h3ZK{td7vd(U2ZuSVA;zxB#-)=7ETf48maoSx?rJd(G>pM*i= znP~jPudIwNb#uv=nc2y8&K#L5Z9o6~3EG|1Ych^!wqsAdIF>var?+7nZ=oMzkMn#= zJ9d;U{`6VKpN`R%-N&3$$*WDbjII;@g#>rgncHYH)3)tbT5?J9p>qTTuy?bf%lU+U!c{aeq_bqTdGn(u`qo6mn6@`I7T9r-&%el+r->l12Y?8rNO z{!z$}LH;h}CyM-7lSKboXE%d{0oq;K>ji0my7%alkWd5|MA`^Zx|-8swir-YfF=A|JY*p*AK7`Sw2l7UW+<{$=F1i~JWvwh`$awv`OtL^wK1v4-{AAVhx`%b z|AhQsME-u{L)Sah#&klyv(Nt#^2d=siTr7ipN4$sx`*1BF38{L^PffjbL1}~|Fy_Z zM?Q4@Lv2h~}|_pV9wcZ)-R4e(r5`OqZ=C zT8)k5kO#57Y{d4$x`c6z|BYoFa11t@(b#Ho>Mk#MN1xlQ+Z^ur?flOM+vcC{XPo(vhR~E zQ$o^=e{Bcfv7dDGRWiT!30X_@%v9v^!pNl{mx|n2kb5|cTsP#pBbSRDYl)u8 zNA8g@ay^jiiQK)&v6kqW0^cXGyXl|o4(*?;P4gX#ts+fTWmnQid_vX~wckzOWH)^i zbh_=%-oVw~`a!>0S5`tC!%ACX2^$K?Cgc6`M5guTFbu6miyR$#vwz=aBM&J%^T0={c;tqUY`9=hBexgZwSX_e1_RK2>Ch4&lCCn$Y&vcEAqFMpDRGV2>Ch4 z&#TJ*VgYg$$UlbO^4-}xdfr^Vq~}fL_xJ2metXYe<>wYuWnZYM%Kq}Ps_cu)`Mqj) z_PN!&vwzm}mh#R$`<8#XiuF|*Qf9IUHSZbzUHsN~xCitn>u zPxh66*X{H5xV3zJkK4-6{5IKFHY~?CuSfs#2|cpPKmBEU-`rbA`!afDmOFd&D{p)` z$yajo7+-T*dij5+Wt4x@+|Kv-<70jQnfBfC=h8-%U%r^=Td{PUZ+Y6t^66>WM@052*yQBQl$#~zg*?0La@-6h`ryZRRc6n+H{Um=ljGp=F_qaL|KvemuX}9=kB+L$o^j_RzN(nmgGcY+chB)Y zSqpfyUuE`g(p*vZTgs2$Qknf?Pgi;DolysiI~DleO12+7mKNhX;hFBM?W7&_-&UDj z*y(QHk4UR0S3l$%-TkKWV_t`EX2%J>kMTq6-nZQUjNSLi>W8^E(f4-Xo{f9?+{?Ax z(|rDm_1QB!-s97d^E)cD{a@E-iytSN>$7Kdz1Oz}|37x@&i;A#o699{%^7{4?;XM^ zdF*8O0p;h&LyIO%@+J2iSbmFZl148+KBQd@_@wuSLB1`i@)ca z&Ug5l{2<@xGHHJ05AOFp$G5sNW_PxH$8YZ1oxQ5}1HS*}dp1wpoh{$Gk+fc#InC$n zkx_n%G%xAlQ_}ssK@a*SkS

    {w2+vA&t-f&UD{;(#~f^yR#)-{dLan>;>@;`3{o4 z&d%GNeYQvM@}p}hLp)X4b&Rur8@of|ki@^s?l5p{`+un2;R5aZVfm)rp_Av2w7K86 zr+O=u-Jz3icj%P)f!$(vC^YO2AJC^AfqbalA=R^q`|ApC#O{!K4R(jlo;>UhXVLkD z{&0)kp_6WRSZdlG9t^cRY@$E(8`JJEE!ggmn)x$jcUU@2><*nG*d0=JyTe|de}Vq& zMdIX=vO8RynczLG+Z{T2mg;r~;sQH~*g2}OJM1k?2xE8X?74tn@9^#|c85;7-GPZp zo=)iMb_e=h!FGrDO}oRS)^-Qh;bh)#u{->5dbC&84-UJE-C@o2|6O*6Q?z$7S?e-! z#3bx_3H15hbqka|&zGI(&BgZc3bvAs^w)B+J)FfZ;Ib>*gsr_->=LFuPV9VQdwEOs z**?Rc<@7cR@cg+lJfD^Gjd`8^5)%TunYezr7L&GoVuhrA{J5N8goIZa7 z`?^Sfbil32-URZ-Jno5o<3+B~9j4Vuzf|l-cTFB&_dR6`_&6`xD>je2usyu2YyscH z_V6X|W~8wjJg?#BYco50U%=1D2;(&RxSu(?WZE5uo$l~}4&E2yqP@B3|Cqj}^tTT@ z;_$v`yTS8s=$>>$W&Tt%@*DfjG_1Wp)|-dz;R1H9O;5aE^Hc0BKTnMDy0JYR$7c2; zyV1x0jn<{X(Z%5{)S?<<$m7zV^=D%v9#2@$G8rG<0?)~PEH9xaE zJVm76*QOH&drU*o`d6t z75=Vf?exy1iVPw_Wfz7ii!44 z#P)CkKjtYvaoy$x<{3N9ilpT#nz|m=Db@odYrRP>_+!sdl1{8kMt(CKk=vkF!BtxhxPb% zZ&sH&&y@o;6LW%W51vWBne_FKdhP>l;Ju(7ya#lExnLAH5sU^WfHC0RU@SNmbb@2R zIB+x=59WXgpc_o|9d*=a&#<@i6?aPV&FqNV(Gi|jlwBK%|P1lun9{s|#<9##i-NCy+`h9Ef1bc$xz%+0&eSs5kE!J5D>U7~90`Ft&+8Y!frZ zHbEQjC~doAwCj%3o-4*SF*}THq7d7}OtDSS);mfY?-=d7bCNHg(Y2w-4pZqMTS`?wv`6u%QW!<6RR-P^Rtip6q{Aaq1V7?19L=9B?O0DTr z-DiCtc%@v{d2}-GOrQ6r-m6GxCUKbadv1re&U3p78vg@sQV`uUh!$C^e9JsEeKC?g zW2)e>(j_GY%NB6>irR6>I_t|=@uX(0g|1+-imC}>9B&es>=kjI7psTDo-aCl0CT*= zwK>o0u^AQf2M=x{5)sXx~>_I_MGZuep%egxNfKVL}fUk8~bUyF33I(yo2zzu=l_ind5|>xqg7yV*g9Sc5Q~e+2J^R zc|i>IPAv70lX@tQ`Y0Y7bwb^hJ?sVHh~4&;BWByzj_7Td9Z}3xb8KVJhiw;$lP2P1 zC-(KVtd)`RELpc8;j3;gJW|5FO5S%YbF8YrF=#im8+yFr*F%qlHrJWB*zZ;1d22se z2cb1xNYHk%4y5Ul!@2EpH*MF~%wPP9_-K~-dv}1$uWOzYmvQkS)wA_^cc%V^akXKrt@tft0~gu1;~2We+NjRkjx$Ce@}$=xR(+E% zy#L}m$K%gyfp>3Nt6XZF^1X{%ea6%U-mIYHdKf2(cG-n>2o+m51p&Z^Nb1K@)>ud zcPi2|#yAdtHOI+2#__K4yff)(c_QN;j*VZEhOlqw`~P0#8#Rw2j3=}O{?$Jk>0kXr zt~Fe%xt<_-Xq%kOWtH^gimL3$T=i5*BcC&W^Ye*Zqz94tO1|}A*<0DjBa$}B7XkW` z(4?RL!#Cf>^{i!Gjf>ynr+>AipU%wDH1_T-WN)3u!N$5Oze-nr#%1Mse$tJOJdbt< zX}GJNX8evD6&h)%mOXSNz4_PPlW}ahd)LS4e=?Cdv7~YNPG73{J5Jg-LH;?Z!Y^Y& zex9vALp_so6Nett|ewPE{!T&n9wT_Ma8GdXQULOF#+7NHA6S#F?z$c^#yl&3Xb9>o^C$5;F?zXgq(#L=(+~YaP%f<&R4#Y%t12P0C%xQXw}41_9EqkZ|iI zThM+HQf^bO>0uJNNzl^A5aWg)2-gph=kQp)e+pce3%94_DM!4%XCQT0%bN31>|DUQ zlXTTiqRrWnHHNg4Y_!wuwAZD*MVkp3Kkctkn(h2P*4Wr+OWL`ZudBCNwD?Q63;*cn zEqm-0#;JRjc2T|4yq8K_$4J+S9oWi)c^ zB`52{=`;G4JfTfasbZ~gjBU{v!Y@33tetBd7dBkx)G&8?qm5_F^GPm$h*s@om&5R@ zFsAC2P586LZd>G(HO4`HTwn~s^hf$L!T#ud3hmPE%=6a!Rp$C+5>l$s-*9ZS4KUul zaa2yo-ibEm5lKJbO}(8Tet%Olkx2R_Et1Mwy9?ZxdVL=Vvuv+{`EP*n+}{dPhh<4S zjijmD?F295jo9ZJLDG+2e+$+_^PGVFZj<6yDlAic7bwr`ZlG*qB>9N_;k&!~vK-d2 z&v8J~y525;VY-9-(Cce;Pu8DG9cQ}l@WVoCTY~6c1<|=dwA9yD{S`s9@T`1k4_N7z z`t2&)2^mLdwNy4n?P9g?eY@?CuJTB82P5l43^Ss+OH-JAk6P_q#EastxBeJ1WZ3Yq z_NrE!LX`$;kYKwNX-z+8P;<%C+upw@-6(Ja`xlwC+?Gqyl(=iPTS@y_Tq7S@3ixCa zNLRTLT7RgeyuuX!^TtifGk(i|(;xDb;go&NBwJM;+-$2pl6JoC)C(mmJD8vFNqvHA zo(H}EXoR#9mBVajT2bU*#8mi|iosx}We+D7&^daZHJ!e6CVZuwgpYZ6DZmrQdKX9$7L=6jBm zTxs69#op<(>@iai%YH%ZFJo(-a0Gh=wu;W%dh@==u|J%kj`*0mHHPP(RCYD#=ldO7 zrvEqVS)2FBUNjE+^-=Wi*@tF%4Cg8Z?M1`7DzU4OgM6eCF7}<*G+&8k*Taf!C=k{y zn)CEb_5xm&sGVO8R$sp7$aw5=vX_*<@%(Ick(+0ABPs$J7e?NBHwDsD+D*OrkuEse^rah#2wVk+);W@E9kGZb}C~nvFK9(46(jb3Tc&g zYDm0ZS$;R~GoJUli}=IlxZ1Ag$!fmgsqJ@XjIyQ7bKA!M;Pcom*pfQy?VBiHj7zH3mB(e|;PLO^SaB3}?q%PrWWq z_UI#jM;TPh|L4>fQQski?^pat;cry@^a%bZD<322r^3gJtaB7@H-XZBrOwTIPT?ky zvM?(T>4Z|GMPjm=u?0lg4pETgQnp5PSgEj3p;3S9~T@q&GQhL4PLyyV#Gc*POrI1_a) zN-Vk9EctJ>E6TpE`uqZ&OJz&=g>R;ZSdr(R*)FsR&9^h#b#d_I9APv4%6>;eo9;P& z2yK>cvX7C_DjjLJD0wq~E>rRkD-URs=<;UyutLeNQ2fK{`DXt5TG7RdFM9I4Ttycv z|HdkMs`77%qF+|}YZU#Tpy<04{jSpQqUiIApRQ=JQ;NKqUnN}#?FYs2Q>V@R+EK}y z9g=ApS=PTKRo~==>o6-w)FN zc@X`xpy!Mn+AEM#k+Qp~L31r{u1*) z)LJk|Pg$~qwzUc|c!%}AAP|KWv&t*jk$EZmDTkK~L}Z(i0Y6L2m&{r`qv)D^y$0EU zzw_A%Y%v>Uw;9wHLBo`9{btGr(<*rf%=ksd{?QSlVn*2m)&I!6ipKyqLt%3Q= zK(Py)Ho|g_=UY z@!Xhtg|!MR6&5OVD|9IY#0%ALuho0*QH~Q)jZrf5O4Bw|S6ch+(-4+^xmo{9~NcYCMBO|keb;K z7ddfarxAHGT|TDhVuP*EFA{#Pq8BLtRQl4)e*O~0@1^wDD0&Yl`YuKPMd_R6Sd-$X zE56L<5_vP7^HZnIblpwy&2nm};+y4C0esOh-8Vt>O~kL2KPia*1Vt;qZxCG)Ew9$2)&;JwOKpvph}~gpyj(@7tzLCYyoe{(u9?xDzP28$CB$uYOfBJV zTQtzwY`bG>@%LJU13tI(P9vn^mgE=c@LHp(^(=zcW{1c#)FM2~(x)vB+o`?Y{_K!J z1H<=cTLr-E&4!=9woeKO7Z*M?ued_|lb_X8Z2J6&>+H`?W8Q+)A>ysnEtSyy zzyXOwJ$_sJvrKi6xkvhQT%g%U=jSy$m+DKpgHZdQ)W7A-mrBN%n>hn!hB4Nh$_u8N z$^V@%puBVz0yWn*qb%b8E3nIj?3@w0w~Bp7mT z6d!CzXgEgWZw_O_d-Q#*%=J7nzfabhm^=yR)p-BY!$vi;Q$ zgiWW~f5F)<&@PO`H`|es_-4B-65pi)5Q(4O2H$MAMbbChagq4$HvAv2_+)!=A@O6j zgCqHGwwojIiw%Fxcn)YUOYJ>k=8~m3PUwtP^*4FMr zMJh%}{P-2WuYn=@QL1jNg-?AD!k7N0xLU4oeCel(i}d}+!$&Vyo6JRFz0SGJieFL)(sQY_C{&MRL%&rA_r>r$#I+zQJKVw1hN9ug>M+!kOyT7{M z7vwM3Wni`t8OUGY9jI&desph8;#KIqp!oYbDC7A%!Cqi4h-&V51G8Pc9CVDlq@bUN z3f3#ERamL8P@!9)OQELFkFu_>uvTHE!a{{^g)W7fLcbBNn0ke^3M&;BDs(G!DFnm| z`rqSoW9_x}m+Z0jo%YxHU(Y1pSo`~q5BR?~ig{1EO98F@@gJZp{cp4V5-)t{hq%Oj zP5cNwOVLq^Z^~1b>HT%Hy>bI`(m#F4P}lw)zR;%olcH7rWy+huH_P2u741?A`_UDB zv)s*6`er%sQzdVfE5B6o#Y%s%k~j1HK@~nTAIB-aS?+va`S-BWuTlQxDSEP!U!!Q5 z?;+var051vXtNwYq4Y`fx;v-j&3xFbNEbgaD-mg<6U`4-#oVY$*^1LWT zcTxIoVd4t8gfHn{Tou2Ss7?p>{|5x|CEr`+<+qi7f%ve}k3d-IvLOBJApM#k{dGb5 z5$px6#@$BFKvu4kDv}{T6 z+eb7O5bmql2;_}J{0hBgji6AA0)7}LI<*dNOU#5?30h(%lo!!(V8%?SirLI-JzyLB zVoFN?%B_dgTL+x?2^wQ>&9}zJRRzT(K3Z@%0ud9@3=xQ)h!P7I%S@0kL>isPG7%J;twz%$Gc9HbJB$8TNtdWnH; z*efcEW|YlXJfFO0*ew3u@%KY|DcdssZqnb@UeVX$A4RM7I{%{N-gS<@oBo)6`$X}~ z;)2LF*Gri7U9Iw?3m#Ck>vn!+vT5aITjTGdENui)lDgjsEn$5f^n+G=g}lF9@*K<> zx(;x?ZUYK>UlUB#FXqDQWAF-#9$mb2)}Wz72aag*I#N4Y-T%Mry$gI))wwpl_sk@j z+#rx}HNqgM2@*^oU{c9(AZQ`RfJiH~a*`0Di3Erwwpd6Q2wF6#+}d(7oSfJVqke*WWia!@+?BQKH|4{TXTVmRnpv*`^m!==N} zJOP~oIs^ouJIIxK68-?(C^wQ3<-##^2=fjeBmOpVd@ETlz)O=uWV%ZQF7rw5AC&37 zN6-N@Lwm|GGTjZp$y1tjcroJM=BKMd2e8W3slHujmk)i?-`f+42F*bCZ=MZAULhDL z%&uaT+q09u*S@34X+h43E!PS(wx>OilH4^-s|0HISf>?-p5vUSm1|JCjqm?*C@Y{z z6D3#TvfUHq9_>nb;qr3bE&t8ux+GwxpyrQvTmDvYrm<+*J>u-5i;@muxPbbBA- zzEI!05UYl@qkY?8QJq(vHs@k%{B`4}*_~I%#9!C1ApUf}BISX9v#*{}AAWWAG2FQ^ z);?;+ShQ&yM#H@|e)M>{DTbTG)idkI!cC$YF*b43%&}KaM|8@M8 z82mBb)8n`ItBT+0<+})UM|^ERC!T%?>+tHF`omLm>R-A#U=(o?BWwL+`y+?=W_#iOFqw+51S)-+`e3=f1>sq z8#=_`rd9E}VK`ap=&^}-?07O)I@1e=DgzJWKKLcb58N#?_AI{ZQp6GWERDSo;hF=x ztP98==oTKkbf_}tW0VcvAK9kb#~$YWhVZ+H5aF)JJMTzb1i!n*dln$>Jiq%%_``iI zV|kxF?~$|v+5r>5X9k|4?>TV}U+MT|mG!8@=4`C}@vV(@)_c?J1FjudXC0DO_u0I( zaGY-sAEZq5d^R+#_LSu)&ZF0(UTv`>@2UIAV;$JevwwMOTvPJa@XT3QV`sI}@#{a8 zdtN#Ai07A8-)%oQ68f78aNewHJ?KwO`51SFLchS+#H*Rd>N71zXBHQyeT=&tv%(S2 z>kkf3RF#2epkoQ~%<|l{%yZTfez5V@GEa*o;^0frZTqryZu>^uhm>cD!98g9<^`(y zWEGwZc*a;%Ty3d7nXQB$EW-UiXVvI~PeLcI2YPg!%8-Ml(A9lO9dz&+=pSyxykiLRWVmJ7{5B@`~>XYes z?!&Xtio1N^)@rpLyv`bOaH=)*V4*ea;O{K42g9s!2Paww9h_?&a_|o=w^#q_fOFHA zE#IkLe)!(%e>ps_`rmD?O<$?4pAJ`sSHF(;D|r8|b#U{yI?IlI-{RWz-S6M(e&wKZ z(;&;B=C7Jp{o)nN@aixA81DHTc7vOpnO=J<+!giNv1rdy+TrblKJ;y)-D{3lhgbilEX=bN@KHSN(A9Mx;8%B|jC_BB=a=_R^z5<5wQKqy z3M?CUpe(X{weEv{2t4w>)kS}S-gi7LXb0A-6ZDy&Z?E`Y?#7B? z?gbUc-31jx-D_8U;C^gXyxXIOH#}AGpYHV)!`%*S2g--LM0G&JQP>@ZU5pY_(|YK@ zW|Y^4JlH(|yG7u00r}M*;l!*u=w7deG+YmOrUb%0IXZlzuhiEtZC7D&N7~g#BvD zgWM?=Ys0jaf6(j}R4VR$m4n^W6l+64Ws`e<<@?}gtJzf<;(lyp9PC3H)~PXj=6+5It$7@Le-F27m9U22!7c4RLK-g|I=mU*t0Be`S~KAAJDWrQ zT6SzU;`A`+*Wo?o@R7|Mf$vZ(HJMk+j!kPhx_MWn>MpF*!`NQA7w^a1qm+=E#}H43 z>q5Ar@GV`6rD2p}tvT60+jDxzEYIW6zx@&Jf_xr#?|pQW((%#AyFAaeOs~Ft?`+Rz z+z0sCNVJoduqd=mAHw_imZr^zCXH~v zh%`+_J8(FBl>3wZN=G7SS$H1B^AkLYhh|g{1-$>z%xcok;2d`Q3*nwCe~$2co~v|x zj&s3nun$Lgtg8_S?`(tOu^+mt`j@c31^yX!8_olNHag7n<;`}_$EcTWXls6;*sI^e z^A#R9o?6sP?Y?8wQP$7IckZwLD|FD+AD&q~1K+9v`EeHcv50r}sY9DPaPBx8aknRp zbH9VK*Mc%vjkYNX;cdiw4&WI}Wb?-_IeBkj^Q=|<-M@!B^l8nfpfhnvJMPm&87@Wr zcCLy+y^e90;XQv3P-3#!Y z_f@>_ei-hQmH+7;pbV&Ksu=8cDoVqf74NYu*Nkpy*$fG_f#rA<@;n7~?L@^j;0wEo zIIT=ysQ8on0_x{})N|TfEC1wOTUJT1=Bn+mZ8m`Bfn!TgQ^pudp{G;LwMRU!#rpD z>))dR;hsOVck>}Ny5Z@{_uOkMcxOJ0iL_1zvnF4{!h^6&nj}yxml@Q z7Fp5XZLfI89gVgj3g69ssCjb~+L^jZPt_#BK9O{kU9|NL9@s5j`Hp+Z%Kq*|tzGQb z&(4)N91M0c8?#K^*WM(N;FRjCicYca5QJpNvmJ;+f8Tavv?oCwQ9Tp*nKGs0W86!IF-G+# zFNJ%~?Tql8LHkfg+My$x6Gp|l6Gi)wit-eTceX7rTfmdSwZpSMH)JBdD-^3+52+mT)a1*vDlwfqU|{+s=wWMDi-?n<7=0=;ukEb zzj>Kw_bp|f7a`B#{?4OWYX2t@$NGl=o8bqtYrW^D37#i_*8|7h+V%B#ulCX=dfo>9 zkf3=WN1Q>s`!U+$6Ocm_L4%z8B+6!e8_KklDHwmYXWQ|}^zgmas_Vh5g-oiVEuuRG zGA_oS7-_o()>VDa@4mKCysvFvXpeWbby`E9>vtgb)5JpGtqpoE6x3}l;X8>jcW&_R!oDDb^a4z6Hz_vbjfF*z!Z`n%$F|M+g17iGSUk->dQ8?0`h;awT$mw#-5x!DM$M^&~ zD0JOlZ7)pooNbuyi9$I(|C`%AH>=Ur7k{7WQE>O>iRwE%qjA^ALe)~e`?e{b+Wr}y zUDgrpEXVBQqR>yVZrpr0yP9q1qC>N)F94RZ?F2+QY0hGQb=_EZJ^Heb(U;i~&c|q* zAqO_I?zBMe<-NB_O8@3YK-|&Mj51nX_d>d7_piU@sjs@Gy>_R1bUPlcjG-KDx2i{X z2z)zmsXMi<>Yx3}(q3&J7j-A!gMCnQpsP1T&gpKsJnIb)zNe=cec&Ax;qIH#rg{ni zZ;o+x><7Ht=JcEboSiz=lMTD;Di64CthBo~SNzWXctxl?6@BB6Eny9*=qJ}emW!*j zxgmoz#8vKxj22h3ry@kd_f)*)o(udW+PlXsu{AfYJm8+V((X>Jj6nYz*N|FyknZ5$ zs^M{!`!sw{g-yfvRP5LAE#UJQcoteh8YZsP{k+%m=H}-RX6nia_wUf2p-rv1u`*JR zS7kH(gRfnSM;zi$d{2c{!}nC|)9@|ezZPMgMjQG2mbW(3P3p>KO?JF-Wu(UU0mzFJ zA@lxOiEWrzdDy)f>4-!8_4FPTeD!elROsPs5#dxJoC3%M$6EGp*8H#Rr|~R6TyH?z z4BidnR~~lXjQ063`rQ7A|2st8D(+?Wq&sINH4JBw?oE9|sySch;>#%dmF z`Tb_tH!MQ>?dXs1M0;yTn{H1EcYE+&jYpQ*MMADaUG}uWKGLCjW}vL14d}Q$%e$XX z`J_`f=LI|`pl$m7`{%_!{rJz1{QNu5R$cgh?z2C6edp};WBxPzsk6^c`1#er-#zu! zrEU8kYCZd-mA6iP^+nE|b!YDHr=9aKPctgCr*Wd81P?#@vtojgbFgZR|J34E&@r`{ zR1SzK#FA1S&MyL_UfBBqp=v5A8xT{9NzdZ&45k8;$`P*SUWfDN0g_LGupcArV*oLK zm}CPazjiPqzc&1lAN5!fPmmCe5b>>8lp*A~Q;m*EOmqa=JrtNr%jSLsz>F(MJLc`O z+jW?65fCi0&*?DZG$7jb>{cCS><7gBWOkDdGhPM6p4IFwyPHWU&MQ>dJgZc>S_EpsrzBGM{UCU9`#kc*QtB(UZ?KH|5|k&{__@E zEXk=ht7A+86k^2J#qW-{SuZ*+JK7z9@t5PX`nv_+`wM>UncxI6Hd2A_XAA$&rJ9v z6RzYwpf3^T7B9hZ7VO}Vc==MX%p@%yShjSru#zk4QW38IN|RyHNL#AOEnZw&Qn`O0iOG<3s<;V=(G z*5Ix82KOsqY+&4LuiaI^v4B_iuA<)deiL-dDVlyjeV;u1Y4Tx_#hHHQHfK8K%g46e zrlc2zn(zn{-p_;&G~t6y_;3?G!h~OE!f!C)V@>#Y6F$L&Pd4G-GU5MZ!l#+=J4|?% z3C}U%b4>VmP56B#{GUzu0u%m#34h3hFEQcEO!yB>_>W9@r7zxwZ*LzJeve(vyIZLk z87t3G$nqh}ktM|0;Z#PxuV8Fz+#7;2rQ@Q==Al5N9BWn4Sc+*+!#V;ZFdgB4sjy*P zf$^g+?}6^>fxZxQ)UguA18_cN2;!nH&vyYwyhHN@bPDJY5J02btV81=hqT&w)R%Vf zWbDWP0z3BevR#=4oO*<1{Qe6#_j5?|?*eDMXl}woy37wcB`(wdW8enwXfV6&Gxb(I z=su6GmOfXw>7HF_F*?@;g=OV_-Eg!p`FL}ZuQ_x*6e^wG#z6&5TjEMKs6 zx#$7|^?!XizgH_h8qeF&NE~HQYx_m_*z5i;TjSDoes3KZupdFqa|x0Luex! zC@jtULL?u`do(=9Z!{*f$9)`m!n0GPa|ReNZPYJ_aSn|S%bxmEwj^v5=!ta|b6eV8 z4fd-~gJ5fCsk!GG9EfCGOD4+HjJ%{BG+7UF4tAEO4iA&vQm}FTZ1{pd(K;GaEGg zyyhg{jQD61y{rF`4W_>h6b+P>=sZib9o7L{EG;eU(!AFdS986FwuON1kR~XsU_Yfd`R339 z?~Ls0IB>!(X@T|UoBO}q!u+yF|9<1Zb)sRA<3Neaeo!_Dy&VU#KcJE0KnEtn5^3Sz z4|Y2alzd$D3sZ~!h5bhnA|`^6QPb!>^;_%Uk9CrsShrsW&d%W?DkkLyW4}c=d>9U* zuTQoC_QiB&hT)FHA-FqjDD-9z!x`z}SO>TUYew<7&n^u2+1a2^o;uMj&@-=}#YR-T zW5l|u;n+vR^%5KIO9`pBiaSZ_EJMx~W4daj8h3UlAaux|of)p<7C`-+HSb8#?^Wb} zu2!qsXh$EoZH;ni>x6YR9Y5PP{7dLuZ>v%^D!3a(zrRkudu}+!4}XDuE1na0EO_=! zQ#PKq!Y$Vdt5nUN?~s*m$p@Wbo)q^V(jV_qA$=ImlCQyax~lv9)>`s$uZsenS6AGRJpMlN`ab0O_mKDVa8KgB zxVtV)Uwa9X7w>)9`O3H8|4ry!W4z?K`wx-#r}2$W;Tz>Tm7iO%K8U+waAzg-xI6Xy z(8Kl4-*4&MM?LUih=Uz*fe!c;xEHg^f;B?@E&=^+9sMo=-|rpY-a6X0i1*ys*Z3zh7Rhwkvb5uE;{%W+IL=kWb%+PJCUbdt9ip z=Z$ph9}ZaJL>U{Tm9cfWd*Dnc%h;f^I{P=LnQxL0zfqvPX!^1f_D%UXu71WqR^g3$RB?6c0oS}*wysue)xZyjUvn#9kAK*F*b0- zFNu1k2Lc`eq)PDsypW_y~qn>EOg@BanoB~e;q`Wrgp8Ur50L9GJa>xnpOgSd_dMT8_m`1QOsQ+{DsaX# zPfvfz{W_e_`Fir50f_Y(?S4VD;YmENO?w9*<4s|S_+{h@OBt0Ww~xI-H*e2v(s^0UZJYX!bU1Diwx*U}?3SwqTvaR%hF7vsu@LtPj}`VzX`u z+Zy&P_-XfG8s*g|!Ibi;?C(+-Z#>fQmAEV?Tt^`;%LV%~;<6kx0tf0fBY+t3pPKMD zOz!hc_*oOqa%$wa+r)2;iC$u&Ge3>~Pnz(5HQ`K~(R~1^kOENvYYKsyu;h1D)6yk{ z<;9B&^B=)2?Q$$=R+b&mjeeh#l*r1L73MFd-YkwO_3Pfv>nFyw3LmY_0ogxLjN1XV z*e+gb(2N|=t-R++hA7TJ8UuXrk^E&{#~-_d+l7T#lPM|;MAGuipYE5^+~NXx8d6h1 zEN7_-mM$yIEfQJM^-X;I;{(G$KAt2LHuY+qWI{^%WX++&bU$HNy3B%kmynF`0?ijK zXTfsdf$3?zWGm_@%~nwuCC)m{N056xUsNvn0Mc(V8fmnPY^+@b()4srM@s`7I|M)8 z3xS`q75vaH(#WW3$OQbe&0w5$$<}whq!s>1rzgU8;AlS;-V4vUqd;;p!=b&-6EA%* zmuLM4WF)MYWGWl)aVeK}h<^0yim8xM-qiF8U8cN+e%gh;e>LO>>K8gCj#Fvo!PU4}QehQBfT1UtJOS z{vpa)+&{$o0#8EjJq0=GG~^|Q1AcR~a~Kw-F%aK8=oHr6K`zneym~n|rs{G`9Q=9> z8KLOlEBhMS)3o8iclD!B5#VaW&VFp;PQLBgnOBP%m=!c-{X4WUgrNvfy?qRPq>%m>lHzl<)I#Q{Je(u zg&6Tx6Hc0uJ`QO!;*7Tuf8B(8JE?%ZtW*RlyZoS&K)sn#Gxty3&9V8p?P1c*&wY2Z zaDI-votE=+y;#oE$zC4|8kB^jLtPZ=Im?2~(n&U6$xKe$8D*OwIy;R{e8rzYpV#~C zgUb*Br5sawwX)h*d1g||ErIn_>T!`l6cj;JGb~wnWtu?p4Ck>FhZY@8o?+V+%<#Y~ zO`C8darWVSC_~WncHYk>I)Cy*6;xU=$oeNT2q4caDJv&MGv@U#$p>wtX3mLt3e*qu zBp(eG@oJTM0bzw{JAy_V?K2Id>oq;eM>EiPJK>t1C?Bl@ejQ*Pn9V~kX_SxHY4Tw> z+$JW%!&~o-D*Ej1bR=SauU>@=0Ipk%dKTrJ;TzZCo@dU1tWna}ML0X^rditGc+=9Z zz)tQjkUAWV@)+kODSJsi+XSC=;8O%X>%eE5mybdI%2crj#tNCthPh5P;Qn|8XH;9m zlt#OAT074ywOTRff~NrToh8II=d_&r3v)K+;U4FW1pI?c4KRY1%@!rZW zkq6Y1iDKw8H#-gYe{QqjeAT_UxAWq*Pghy&XHO2q+!*B89Y_P`o=!oo)4mDfaRT=G zd)K^FEA6erA^+)fuWi<$cS0K9a3Au4G)uH|6Y2D)&Ev-6Om6HNJXLsH8DC^7>9^t= zGEDN{$-G0jA0k}u{Fn%v_Ty%ZuNl9jhp9d<5_p1^%LvUW+_f zjdU~3zf$bC7;z`&Q-5W(9IXv?Ha4OB)lHk$US}QNUJJYJc(lCM%Bh}bO>kFBch>vb zwfco}`Wuu{v~+ul?oc*jKCC0)+_iRREbHNXl*ju~F29fRc^}H@_b_)o5BF_~xoegI zJl*Ows0uyWj&j@~?yvnE;}-w3DZ|=Xc5@Y_qX4iC=edg%CH?XpU2~XgFqbOJ^){62 z9RrlPU!WfR1@+-8)Qi?C%t?F8xPG3)xGq4toO{*h@ma>Y$~eZ_8`7;Pe^s)K4>Oc; zS-#g!X&vIqs6!dSIz>8pp1`BE4_X>77 z^+3Pe1N}-5^jAS2h4JwoK&}U@1&qTu+oi+#GXT+7CUIVh_oLVWuLV4ZbzHOc&~+mac_>OxJOgBl2$n zWc*(TWPElBxIsXdfZVUhcxMZo_q!6e3H&k=fnWAT9cEB&*U|^5g#*ZRGyyUl>j4?h zbvm5S@jTPP`4HMWg#8sokADjw?GNa1{yIRO&v6QfwotpD2HBuNhJ#;dt$;28^8|DX z=nzm5uoeFC8rmdat$;28^8|DX=nxP<^JlYCS8W}ZY!0ZLJi(HUb-i^1bk$z8UdGL@ zN5b9cq;j-kRcBYq=sd{a3t z$wTBheNtlv@KMO?O5?+9G98{YjyvcCUs!H;pHK|ingm-RhE_?PWdGU7u1 zvOSw1-0z`hnDK(2Y_C=b|FZu6R`{3g1shxPlkEduHC(m_#qdu%X)eOO5qBbgj5ym* zBkqAmBmShx|6&vVG#ne^v68bZxy6O$513c*3Ko~*`qiZiiyv9cn|U~)=EaN4a&_EimMduJ zat5n+u9?}nrH_9=;b#o>+Ml!38V=zD|1JDUwiD)Y~L55~Yvy z-o5TN=tjN(pVu3cfT;_zoZ7qmXXHf(hkiS^*tkfG$A0uNPL+)+Zp~y)u};(Z*nMjE1o}{7GE} zajHIkjVmiIW$BoZYGTT`!uSO(?1PSPm{yu79r*c=*X`xN%#ZF6^kCtS0Uh5siF}YZ zNsvX^&-2(hVTMbGp?Ly21#}1qKzGnP8$++kZ}1r9Jmv%Cx&#toxQ=DSUm}igCdU3`7m2Wy|` zi0u*EbLI{A?ac!;wnua$AG-Nk#=kw%*aWNRvc3ePJK!5p#_WI&a{!I?cm{DqTISOY zz`LuL0qT{v6n9s{x*UnRh63+BEQc`rTFz5~t{?iBb1GGqW&Lk0=gawlVOTC{B>r{F z`8)$Z@v1ZKV>y?69J+6%6@{GToP||H)rWmg<(%^~^q&Tp3>c`Kldl#AU|&tiIHzZZ z9r|IQt8lz(Z@$Z}44e@^<5=;F>)(B1{7awCT;KAkXZN-hBpkH)kLI^ukl;K84P>gD=7F(ta;~`qJ)0pI%t}$*1dK_kUFN ztPA&k&$?K#DS554J;D-kaJ?E?U45we;QG}qn`f%w)hWOe_Z~br_CJ~r-lRrVmp~6v zapT(uZ(4J7^TtE42Ys44pnB%sw+}uEKVw19R$~rk!OfkoA3ZqJ_2%Xaqm)a$>->bZ zU;P`=%EP;{4);cg@^BQ^H=?b|B($G#Zwyi%K1cfjxaSV`==!gVPgwptG-{(aN6b+tPjt|6urSVEoeO)U1Qkr}sdg(E~lZ2m0(D z=p0AU1dBgqQkr1&`*m_qdR{N+7`r8r57$!|9@n23FRs%t{#?IdI=N26{J=L)V!p}s z53Z~5JD`p7J15OAf+i`n6@P?H0@e!X5-?9dr+^Lt6#-kpAFrWJ0@e!X5-?9dr+^Lt z0W^Q|d^PWlH}(@DU`ly;AsxarlWxG@B8~>8R3|Gxf_wHOOh3)1c#Pv*o)01}%f&(A zUVbN_8a)q?W9U^uSkPs;{H4HUJ$XU6m-Vt*;0{2VQNlgH z7tNpWkbjJZDat;93skX-^vH7fErGLK(7X&B-o0j%37-$5k^V0x{8kfwugU*u6CGvL zhu=~Y9&hsB)z72D)#ruHM`OxJ5p8B^Q8#Y1%*`J%kQ+HWZv1>*{sK|;jnlY3(0tU$82fG=b?>!X zYM>MOeU?Wj2HT%&NP$5h4J3~)XGM14fuTIwro9FJYePMx;rtAZ#HBnc$i3c=yHM~U zKl*i{L)6+Ve|faq`J*Di$0Z1MMsMUt2fQ;*Y;#@UL)q{MW@u)^(kMUD z4IhTXnR4bupG_a-M~BVX2;ra+G1KHthi$44F3*M|9$V<<>I(1K@3>B@<>(jphF~9G zDE5_xK{qt_zUuo(Kga$#J7iP4>g?cCS9NXsiP8E#vXg*v->wz%Ecd=%58e!;u4=yD z8ScI4{;sb0Hp00B`vq@@ZejiWHr?~=)8$zq2p@64{xo^lyKmSdykRpAJb%soa;FgP z7;m`#I*qq$;p*p^5RP0UKt6OCghRv6=3|6?P`Ow3r@{Qo{y7-kAwmmAPw0W3Ea)O0 ze(`tqK%Xt>BA%#izNSc^g5bx$M9|j)qJH`LzbNSCfT)Uo^gLLijwCq&vCq7O=UxEk zrvhU9rRljH2&l|C_=oHW)Ab{o%J8)F)(6L$9s~glUfUjGcEc>x&OW+Pbnvd}q<-Xa1pOZuwoFmle z$vUbm5p?-ofogR59q$zQY~fzZe>xIolkm@crr|y-16kpH)JB~9x{NsY0U7bhCj3G0 zG}2!-(YqHC%;%yEeWF|m*CZ)lP{H! zl39X@g^~-!wgBRxBny&pt>0(Mg)up%jT44U$SqrxTfQ{6bW!)29Y`YUN{LDOGKMTk z#%PKy3ErJ0#_0ITV*y=tD|(3}G5dUH1tEV;5DJcZwO?UE`UJ6%XPOTb;<6OI{#LoG z4)O<$l)EJUb>*%x27aiLiW7_+44{v4SAyW9yo$wfrah3{#c;TA7KNv;rZdwDSttTBN+e{FD4YYbdSwxwr{S3;-alv2 z&o^yZhkgFMt8SeIXOy55Ro)x%M5VkpLf^m7GeVS=5T7l1*w605e)d`os+!Dpbp`Hy zn{_ATqAZ;Mv1&3(=RPZBqG-gUA7moNBar@`5$(07l%uMM&vv}?e71~Vk%(Ur;#Xvd zUoqnc8A{%3A>}E?ueR!gDch_5Yf2vC=+gGytBuAm0-aylt4(cCuFOF??t*R}Jq_g7 znHhpJOffhgG63lt2s-W`SrLc44cREqu;a5DWXx-zLrT^1s#Z}Z>G2f$mBhLAx~lK{ z$(O4KQ@)%F{`Vl9ISB7=oJE<9{M7HTARop@`DjwcS!sE)v$ z1`XnVj+VoBR`WiN^WvV47jZvFBk)|A44?IQKpYk;BnP`0Jhn}AU zdaCBN9|638$B8@Ors58`>naYr$5-@oSK;2GeB9Yng*%M$aBtYoiWu$wr=1l?+^AhO z1r-A{yr2U2O93CR^lLzksaXv^YrrQH_eb$=v`pMJ#k-1jf*oN7 z@JRtr-o3U)__4Me-po7Ks=!OZy%7$?V`{}9Enb-wf1rQx9je7+C&DFOfN;RCp#b3! z9}WJ=2rC(P38}c7h;~)*8)H!$R)ZJ$I>C1u?#AL>R?~22mUd4O!X%%ah>Olw52rwc zGg^dmGs2mLyW=b^N3^@J)_@OrUckL{&Wa&!@NQTSo>>)tbU%rEF?J)&C?%?)5plr% zUp0#n&U)N$wFu$hp3|C}ke+HavLO-SU`?*(W88oBkQ&i&7Udxk^k$TaMBJN^kYv~H zE{YU)7V)kZd2dGDjp?2Q+?&xb`diw)8Fkic+PCjhwK}S>EMnXes4O>~(RC&GkBiJ_ z`bKE_w`6^98?CoTZKH8+Y+F}*bY>gRA~j7pQMFgA*Q^WjtW)>(x%+VwRmBOCuUGP7@1d#i#^3c(6-&HFD#oBHXnSl22E8(sP zxC^ff-yKf*-8dd$IyoMZ{rWuc=XgSX-#K_EF2AQ)?G+Js+c!vD7kKV-tukottzbx}mn`zhpA5v3C$C3ho?_hE`v zvV@eJTedjAq$GFgGHgCqawP#9yO%D(sHGS)&W|j3aM{vF0&D^|%F{s<5lDW9TO4!^ z+LGYCbR&03;e(iz&HX|CGAgC&b_20dSl7o6T?)ekjv+9%;0PmtE5kU!M^NvxoSuA3 zV7*M7nwCO3zWv`SrthW3_{#l0+D_a)%IT6%9s`G| zLpw}4o$Kp_Fd`%!=}As!yG!|;o(Sz|pE*A|4~=0R(jG`oXE+=#pxx`UY0rHfGM)-q z&jMKzXZH6g8#^<@kA8`>!(SNYF8$9D$G7t=tK!mSK^M=vLS9A$wlsNVOMm^7d&6+Y zUS>G%*vmwi(-8Jl$d~&yJx@FnOaBhYmGZsmYUiZZQ#?Pe)0`R}(siEv6yLk#^i~ZY z?3LSfyzBh0hM%%+WH_x7S|PvV{UpNSnOV{<7uYto;yD4D=2m;x)3?nMdp0g++kGeK z#t^p4qKplsXVTTK%OEIPbHFRkUzdYgU#_T^_F*3s$3{~X75WIEiwY0V2UGu)@Vmu@ zy!{Ez=gRqF#_b;9t8l&?GSEP?J_pjuL3;&oFV5)GpF9f<_ZVmB_m4>Z58$Vn5|lO6 zE0jiGLwg$ZoisjWn-=AQ{w&^6F8%zsgO0vOJ6Fj5cQZ+xdCd~qowHNd}a#u8!n3UIY!y% zdZh?y;U@-k9rw~se=YDmeX=Ep7^(4QAOR@Z22uArO`O9$YR(Y2wA)N#lHe~S23Ku?75LoyKn7@8g zg(BU;exl!A{yxhUsgskni9$bpkN65b$Q1`zU>$g1xbB)pJc!9-=Lyp$+(?`<1s_4~ zwOsMKsORLz*BNNcSmx0Zx(KA9-lFdGAW1&g2@kt$(q~}UlOCj25ighUegOQKwmdX; zg0*m=JLpMHalxMhPU(qqN-c28DHoAx8<2LRoI?KcVZVWPx6j6?mq^ZafyoxM|EUOW z!zi3tVqZKB^LJ7gB)s)jG9twMA`g z6*%V?B<%#=xmL??dWy#otmkTc#oDg);~%bm2Yl;J9k%V9uFb=??X;!0jl4a5$RK6n zmm@>cU66}Tz|Bdd?Ualg-sRi}<163GAZyqWKITfX&h0N=>!4m!^uil_L$*X@vRc4x_A!aJ4yQBtX;ulrQ)(zK9>>uP(V( zy1l3P!Q1sMrj8{`9rCqcX-P?O8SYHvl_Xv>9dE!zmGoMB_s_iyBIFc5`enf+Uiach zm+}SW9@Kk3daXs5m+l7yG06!?IgJ&Z_(kxiyw(H=`Ksh~9nP-ggWY-sI%^;@f6GC5C0_$3al^h`Z z9VeR4rV9z8T0e+Ib6kUQ==D z=P!i4OWNC;-laSA9oo924SU~QxVKW`q2bPsCg8kF!)S->e3$mp&W8Igef&l1{u1EM zZg2YZHU)a>Fnw-zFJ%ZMZp(}-%Em;c>D`23Gnz+E9dmZ4!F{OUXLK)l=Ysc1xb=x6 z+#`;27)PTYRhRL+cg`tvgY?VHMf_-({XRz62c1_zAE=pN{`t*mg3%osH8?$?2YRxg zi+K3O-`NA5?Kq8yr=R~Koh;`t{pibkpsy43aXLh;^fh|~D$?OcKP~8~I`pFt74<%7 z{`Npm=z-4ptT@Oal!FK<2Vp#wbOrH29W80s;rv!W+P4V%1H%4gVbA`U?zam2l>%q} z!G3;;z?t8~=LvkSz^4n`DRAZo`7z(#Ikag?{@_j(pI%2T4;C>?kw zF3TJ9kM5bDG}psDappG-k4ktca;13J_PJ#ajH2l1#&4dqzdn3I|qVVm{ zbrnm&PlT9U_u8SRxv6luK|7U+ljgwxY>3HKj}A08f%K%C81=$Qm$E52b7|tSd?{8W zrTAsm8r1WZzb0;w{DR&NY;r2|O1mX7NaT=H-Cv>@fTve))?#r@O%UE%`1qJq#YL@1 z!SX=nV0Uf=3n~_odKQAP)5>;V<=BanQUb4gN)=5;r3(llkQ_S{bw_b%!E5uB>wGGb zZZH31M1@VG9~g;CIaYR9yx?!x@?{4T59-04&2PVK^H3w{n^wGR9=5gr1p zk+1a8lbl^D_|*yzb%LJ*{J3TcBXU$ndXlqez#rRcdSaVi3LNtt%7!SksrY_Cy=^Jy zIr;FjG9LoPAJ*NPe5lNs- zc&+U=<+Z;x>^98Ty=g(3Gu5L~fBZ?L=ZPzM$52E!IFat{hIF64F*Gqxq?_qFWwm?L z-IYe95BVn3$1qL?2m?i9gTu@pY1H^5jm1dg8Kkkd;dEMU*smFZIn7ASZAM|iH1OIeK_h)Z(c0^(PS3NB9;JHX=$0FzuSrwwBv_c=q zqN{nw8m@M}(x9r%t1!Qs3A;%6$qBF6Gy~5We6Q<@7G2p@O5P6B}a*jkH)SS<{?rVsBT5 zM?SBFX8msVn*8W*Hs>32LAU+@6{Y>mTiY@XC!dm#i=Ga8IyAk>I5oj9_&qzyT)}pD^ zu@9>QA}3mGS!)Z|$Ck|59J@i)Y0oWuEVdl!uC@$_EK#Gfeus2ygqszSO3I1PI_EA> zZyfRF$f%rYBja)`*T&~0jB_TgwVT~C-*&#XQHhR>u?)yciOEbnpXq$LNVKa?4qE4Mh*_j5n zClyQ9Xy=<#>^CckQMamz{WC3z17fX-A*f&B?$AVAd~MGD5%D?G*6zxAKU7Kia`MlH zFNnWxi6wK*6bssl87T9R$>&}{8`gon7u26`v8o$KVQu0J>J91?pwjU%>H+KM$g9ew znWsAE4#EARc>no@{?3lkxN{YC?cKAeTSeg4HgR2z{i?Fj{^5@{EmG~xsB4#s9@|#4 z3Uzq2O__U69ooDN?)E|N59{0l)Oq^He0p3s{G7v`ztL67#!sx_^|c|Z8h1p$x!Vx8av6HrcJw)#so`r%?@WRvvR+aXqG7 zX&j~ee7o?|#5t8k#c6HDiD^vRZED2XD$uyTGI)MyBJW3FJDZ4kAU?ODo^w6u>WW!t z8*|V`-i5X@8*S#D*mI6O{8pRKIm5!`rTI%1Iwnp?;*LPaqQ2tYcD4p$Io{E4`*qp0 zr{TCd5dES?_3B6P^FNou08Eh%{OGR>`fLa%BLjqQhdjgeisP8y!}lzq-eoN#L+h2ox&n^KSFvAH*dMY64F@Rv5&2<;RjN^zO>6~XE{Z&AWA(Pev zB5K*|beM5JAl-5NNOw~J>5j4yWTfmk9cB(uqCMLYLrv7ED9#CpqVYn$~PcH_PmeLVUD2wf-P-Z;+xfH)!~ z;%iLqLDu}s@6j&&JA^#BSkPs8xG3n!g8o}Ur#(#@9){->^zFnEh%67BAmqA_ zlz)#9M}|uIH5vFw1O5BJ8AoZJ#)}b8G~wI_VWeYhC9amrErbqe?4ZdNTWo_Yhy)q3 z1sYxYubqfNtap3|UlP6sj-vNFI2OgN7_NO?+M`Qh_kvNMHA8l*|*xPYm zD%{eLABv-53?0D)^Upk>!7!D^&;J zENk~(A`VFm4|%QW=f&s9X8Zs%nsgXiE1*ljJOP~oIs^ouKdi$KjpzGmJjQna9^y!? zEU(<(XvB{b#|L);(#ZK#nIEHI$N5odo)EZ9FG-}!{7)CS{9bPeT;~6M0+;#uF>nKW zh5d>Vf6;`S=frxNC=E8TX_%)h$sLcXZf2}aLi#jnK`waITX8up=;Nas{lL1fE^Rgb z3V=TXZ%EZbFRXX%Cy!+|WQy#w^k$aYq)FN<~-JOi&UW8JvP zyuJ)OWZS#6SH*fQV*U?Zrxoo5v%|+E2rL*qP0&Sq;^%+1pa=SvB+cuAJ{NSh%T7Ri zqY}#Vfb-ej5oi2}v#}-4co1&|#ODre60lZ4mwMrn{|qF zy0*E}*p80}bGF-g^Z@fX94x?@NJxQJk)-e5E+R?yfuww& zmOfXyl>I;tS93_`J+IOTmoi`b+U`$CN$s}Xm)(kN`2W^+pY1Y@Z1*Mpb=&=+27Y|y z`A_ye5Y$`*()1)>OFpf_!^>!=@l)nO_}BUoQ*+!?yU%?}$?(rOvCcICXPec8LA?tV zAyB)|a5!EN;o+_KhHD^S+-t=gQP(<%)NN(HpS5iW?sFW?bq{^-ue`r@!8EK7z}|Rv zit810tLpv0mbM|z4#d%Ce(D5<>D*f`*Gj&)7kA56b?Lw2+4A;WE9S0jh@VmSmJRDL zR?JgdaF$NRU9!Gs&b#7i#UAAl?>)O!i06k|JcmF>#$eC}VXv}&_N+JYRWYBY$5%gd z{>8o1F{g$&?=!?XV19P#D*K|_xZ1pNE%3&*>pn~Nt@yHAdK1^4@>Y@ky=hz}z8A++ znt2xgD9@-JzJ~TRd+?y#`O#;q8d<3W2;`sg5sl~ zkpA0{DEe;!)WXr>{H=iWUn=ZNg#AK5`ls9gxlfZD=zolG=g{H&CO`xcS}UMSz&rt+ z0y+c)(A?XsC8gyyt7A-p&B~qCyD--$_w^jGwpfo~&hN5?XY%PoQ6?Y|quel#IB2r{ zO9O6{3&z3^arvEV1TMb|NE%&!$2$csh1Vqlm*wb!z-7664LEs7^8sFrIQ<#%2TVBA zVx%{l{6A^J{f2R}r|oH=C_BOcmsseMiPu4J(ZlORlReB6iM|4Z*R7Noj8lL?PSqQo zh*`oQOodz#Tr`rGZUxyJB4g*xPW}EZDU1Z>%Wn+%B(R>VAmh>r$&)5%!&39OR6j{4 z#S*{$BW%cCWx;UZfuT&mvK`DkBwD`X!cCFD*%$MnEI`xS@#rq$f&5tYoap!Q3m}&) zC@Z7AFy{RpG9IG(YOajQFus%6lbplz_cTq~KV%WeCA2D)@$03W!}w6Hsf9nZgW9}( z9U$5^@BR*EAs?ojZoZb0_io944(F!0U%0ah`lqnweIgQj1V$>4wqYF*dj!(YU=0xK zX6Xs4J?qR!=cD#(m4SF4+a}$F2seO1-GmD_fI;0v3pap4-3$69rqlp!kdlE8cWJ!AkZcqkw{b?wR+~;8|k7rOG6N`(| zK1O-W0)$?ttdGGv5#(J^ODB-&n7M$!WqqsGP{L z5lt}vY-?!Nv*B?_=|Z#8{&tB5(nTx)eT-=Lg3;H4jZ#Yf$b+wkoHSU%NLX^g}V7=xsPB| zq!b0be9bnKQ%wh|giHT3@`!Tq^~fOdLrQ9pdvlgAeSiv87Zs*VNb^b&{_BzKzlxaI z4m>c-w>Ir9@Lw|B!mJZe;!=*lC)Z4G4!$di?R`l zmv)XaP@M}lzri@L3_9Q$;~FIf8B_{h#(tk}_>eFAeJ#F#eKLc@e6vG`t={o1kAL_* zUelc->wOp#A8M9_V9wpeOW#&haVva6Ie)+2GbO;Eb`74BHV>^~h9I2J%D+{=>of;2+#HG1j z&}Dw}W0EfO?`h9MdW zZdmu#y=-hkx}j*Tp|5^6AD>2D%YK37{;;^bD0ksfs$BE7oIc)pbl2CFk%=k(We-16 zSoou^D=vIEd5J~_xl*5s=m&g!bVt+U%-f!qEX-eqbGHE)_#c@48uQmd&dVA;TQ}`H z+ot6#5Dq*rJ+<%0*=WLS6@^IRU$=cvGw@@hQHluR99f^+cgd%geqoNFp(1NL24AB6 z^=XrWwePI2teeU3M@Ua>`<%ehzG(Zy5|H*l?LEU`K5)F(S7X#)B6W*^$Cho&l#XpX ztm&Jdx29vX>eXGsF`%y>rJn9r!M(k;YEIF+>Z4}&fpod~%#qh-rWau=#2X0j4e+@K z`}~JMuHUiD*)bIT5&FA0yVcTMY#Gu#68k>~r#w-!+A^e?^W~>756$rF;MSrJuHFIv ze*~=)y3j0`Z_cf?Tp9&ApKj^S0=^969QLv5@$AxzA@#j+kAEP2Eao^vMx4Yr=00n= zR4c;TuFc8&hPC~9t2Vbk8u`dL*kfFCP)6eFp3lU+k;CTTd_wwU&`#W@uK}ElbIp-% zWxN^ZUm<_~v3cXG1BGW5v^fCid;aRd!1|s)$GSqddTOk?-nz~*y}Yy?Qy={fzBj+i zP&~uL_fEj~_E?5$-+KtYcPsquwG6F3g?;nP!?rSh?;+JE!KWSeBdvo>-&+rdd>IzM zH+h_Z-Wi@b(aKCWI$+FtroVcyOV2d@;?Ybh{!uMa>wOLFY1lspqA%2_I0p}iy6fkk z^WQWMz(Dj=(W-EM9QDJ`e<}(X{3UTbg+9B4jS}GecKAbkk@Nu|qq$~lxl3Fis>bBHtP`vGY`9guwE1atwyX=t8+P5~VP z0%&xbwbgdo)@HL=U7>43*M)LT-6;PIBaXPq_Lm=&Ze+gTRmaH?291)2YY6^c6}Zgj z!vbF@9Bc(74{1)}#fWp9XT+I4BmRX6zt@DDC#gOyws&wwN_XQ<(*4Gr#=v|VWAo5W2JGufYoG-a z(LhU<-Ud1lCoj>+An3T0EQ5_ZN$YXk2}l3k<4%$6ZrXQ_mA)BwcD3){j5~vjJJBEg zP2*0-KVaM$FUFk+Yy010+=+2%ug0DK?RoP*Fzys-`r6}8@Av*@-1*J8v+G>X|E1$j z(NX$JG3HU3kBDO?e!X$0Fa!aAMxi{mrr%m`MI5xUhd>e_tgHNA!#JBDCYi5ECLw$J| z$D+Ozx$5fMN*jiezRvwuLw)JmX?j1aT<;JxC}Xb+f6oC%qrQ%MbkN0v%QT3W&`@2x}{xRd(Z`M)0--0i4JZIBw`5Al+>Hmmz)c=>qv&Kn3 zpY5n!?IWG|wAjAmS(1Y+tNQW?Jf1a1pcms=85v)vz5$uWvoZv4V;*okOIKbOe*6Nh zuKGIac05bwy&KQM>zR0`1>Co}DoHmcaL-7!-mTwvGAewV=GB>1uK4b&&HH z@~o+DxB9JCGw|l)7G9GeW zi~dB^G#17=7+O%TMR)f>%Xne0zh;)pgbCaXdeW0ITck5jL}VTKF@Ehh+{C$j7~w)k zf{nN3`T+G-bIp*RxSs3+&b|6g$bbz9!?->mzYWeSXm=1A^95GDGGvR1tPX^v>B}(Q z!ABZ&WppTbcwaDP;C#+f;r!W#B8#(Q9n?Y@?}wKDig4%e<<8EZ(T#%p30wc#>2*h5 z0mFh$Nf}l^I{4&i;py>__ZWY1?{A?qOhvzL@mz(zh0maO3mLEldADI5!dG$6;9mIC z^?6N$+o{kqwy(~Cc&v(09)1)1(U=EUS9H;McBsu7duMp~vEh@p$6iBzT@YGU60x%qK`QDxU4XNQvzeC2okd&Mogc!92;X!@LQ zBQ9Pe*0EWPeT-b+cIdW#wU1?=CL0fmyB~d}MpYDt4*lrQ3i`Mp^aFyv1rYVc&p+4U zX=dOF6h6-r(FEgvM9`7{G=AwnF6a)8>`!kO^kCt$O^1{0I2~q00pgeR0W4`x{`h53 zzZvSMcF*(xz}JBzz1gqoFk>Yke%Z@)n89;V7h*m)(YqnFi$|IfDQoxH2)UdM{T#vohR=|s8e_2rUdFo3$wh6Td6IU z)?63KRW@#1WG0Z-8q1 zKV*54`$O(00cIW`*C=FpJq0`B#|53|y@<>DxJlqE0ckkWCta53TLexT&GmRle^$d3 zg?dPc9}xJj0fBmr`8?TsCc2M`Yab-HtgvvQI16SFesCb+2=0FUu<%K0CRC&HO`)4>JiB=7_X_2GjZWD|KbO7mmt>) zahl#3f*%8IS}o9Z4}I3ZmXbPgg1%wIq!+wk>B7Q-GOwr_XrIWdtOE|cs8CK#5OqSg z@%=xNsN=_l8;MIwTVYWMf|%@%wv>g<$+vA&%P>yy!V z?*rsUFc|ZFfX)QnUbYuQZ}OFUwMIvGqWwco?e?CDQo)aMCd~}+Ga7T2_asOEo8T8E znEb%R&&ZqaJ;~883w{pKXsi?Ibs+4= zdL=Cj_gYb>z{sM6G$}7*%&-A@uwj)`x%8&E7mN2;6`!lvl&$ouj<)ryK8(AmoV*_q z&q2gxTC^=^^o^mjPTydeb$wXqtdBx1v&iS$pl`q(Rs$?b4#Llw6Tc&8;NU4s=Y*y$ zoipNk?LB62*wTAoKN0q2urGr>-v_`x_3<4!gYcw$H(^!`o>xYU&e?(| z#ytK?&UYSvKBpYdrt89T27yN^p3Qjw#p-QQhjDin@4358wVvJkdFR}>aDKkHZRI8z zf9B;H#2@8#LlxpX2I=?^>9_&u_%qT$`wg%^hdjXd-|%z1pT`*D6Uf@{h3uWO32EDm zwEe_l&Dmm!$@wnQ@>9hBeZ=AY5jSY>aY)O(u>Upezl8mlu;=?A*w2Iga=Z@>OItd0 z&Cn-Y1=NGcSz1<$Id`31tg)G+$Y&;$ATebRh@Q)f@sn z&ALv7eG$r@FL58eEmGO@(JaL?9OElZX9CJP%KMx@;hV(cIq=Yu4(O~QDoCR&;F6}~O?hqOXJ z!wGTMWgg<03b)%VgU;@VQs#DGOtu$sOR&Yy2}e8<5&qca!v|&|Zdr&M^~JsGadJ%Mo_;u=wm_%qI}xO*{tar`}B zjdXeAJo?MdxfuV=`Dm8Yb6>bJ336DR%tMal?gDPiS3B(9MxI9Cv4a+g{E9T?#T%Ak zXPszs4k2v~$alm$ay{bTh0$h@g&p1e$f8VI z*AaFL(+2y2LtuZ|8d{GwIqovtqJGBldwhy=$n??y)!!}-XYw$IdG6n*C>pW0U*aNDm=Jr@3X zix6MjHF)PkYQ&S(cn54A8RGo3&H7{w+*L!_g{HGa_9sk3=f04m7jEsh`258Fi_hK^ zv-r&D0gJCJ9fr zy3iTb_HbpRXCTVHGIH6V0Z-KMJ59SwnY%4Ud3eVt^kFkOXTf}Sy&Hii3J?2_JhYRo zH@Pyr?>8FWM;qQpx-uHzpKWU`{aoY9IFGzqjJ$$uw4sT1LtGh$uWaKv#tp5+V_g}~ zbZnaw16;8H@9)Z(-M($k!y&FYj`knVIT7jFlpJNxIRPB<(1tZiQ^qdbW3u0-%=$1) zo%KH2`x|YRSyuor17ZM`Gd#+cv%~t+DXllVGX6CzY*u@SI%@?U_>V(BxiJ_0N8S`= z?(IbnrPVp?V<)Q>zqlTG^G2L<>AB?UnnX3E8M-+)-d3z$T7$eh1ARUpTBGWpga5>e zL+-?@(~jK$KEFrbb7r2+{g@h7-P8|pte)ODV(rPCk!w%jJ+sk{_i*ifT4My>BenOb zjZt`y#(O`+xx^Ax{e?BGzV(`a%NaFnUCx4GPv-0#wmzq7?an3pZr{0N&DtGH-n@Os zlGSUsFWGzh_9d>hwM&|AuU*nV{`wpTp6Pf_ARh|jN9Al^`>UKac&vCndhAy@@7mIq z7T~!9&qO>Y9(y6jvi60Xjd+-6F91F`;>Mh#@hLgDhcjpD2=K7}Y|7`4?a3Lkc2CZ0 zcqWc4%lQ|;^pOwbd^DmUXXl7zIVWyz%J{|F?KwZe^9uSD$^gG!``es3BW}t08Q?1# zd_9N#WBSNjfd4S(*Z&`R?*doVb>S0Yp?Z#t=DpNq~q!MT4oQ9xq8yDKUml zZAN-PAOS@{z(k{oCu$lDb}Ffvrit$-CZ+`si0q%^ib}l z@jn8+AOGF>ALQPK{QX_xk8`Jue>rzs;_VeL^4a!}x!d?4E-&IKNh--LPAblQG4Uth zIh^~4#1}=pW{lsL>pLmEZEf4X@C@7oeffd5qB5s>jbzRxU6^Aq>wvRoPP z%YZ)y_zRK>a*f3A=jP$=(vHXX3cE9p|1<0+_6ye=STnYTA0=FftL^}Ta zoOy|RQ4W7S#|^iqZ@j%C88VZ>Uzk*wo1FMU?r6}xD{Z3O`Xe5jv*YoLxvh9s<5`R+ z6^|27?-Lhue}m`O^NmZd;#++I_aD4xM0GWu>&;8O-Z&XM`OR@VMjIo)X~xC8vB`Pw z6A|W!pBYine{T+t`8E27-@VZs@qz88=r@cp(ci_rv(H?zkGO8)*P=hO&5HhR*|=q& zM;aq;oA6Nd4%3L4;w}e_O50(MN&CPSnf6UHGHt)ZXt;mXKxubD^D!#E((MG?J za+UT1Z9!jqfp(zp9reHO9d$pTy+GX$X)kbJcR+g)CEE+O5kFFG#8}jKclWoKVOX)# zjrO6#?)RYGSaud|M+4f9;CAB!yU3iZc@AFz?@E$APy{*IW9*6fE zEp3N!{hr);*Kg0=a(#X7>(_rvv>h+pCEJeY?~-lD-n(Sm(Rf#5g^_p@%HK`7ci=gL zw74JT_cgQyFW{MkCmv4%>MrX~6`sGrW83!Q+=+O8f`{q$7T{asr{|6uKO=W*;>6r7 z<0qi3e|OI4q(ix5QOq1;1gM_x>vS@8xR)_qm~-$2~nK->1OXb0X%{0H#-Q*Pe)?~6FD z8~bnVfYy<3&myC3?Lmu1c7h^WI?gO5jSOmKt3cHC(MQE3w&t*Gu25qhZnFeIC zU1}5Ul5DSTL;K=KJHm3zcBJ+3OS!+qvjfj+Jd5$9;&I~Xed3a6M+zgHMfS5*1*pBh^dDP)sP_J8I_e=CEaG&=QXY!lloM`LbL?6P1{ww?I zacJ+RnsL$WM+`@s_Yv&k(dM~qsnMzC@MyLb@o4u(pxw)uurzwO?bhfF_yxPg@o4*6 zldGbgxTicjxfC!i&1nu#YeqjGeYLdGRa3Hd;9I}P9Nkr>`j6}vJ#UNc!g-^41^9mN zjgn{RG2EKQd*44rd)XApv4*p&?we0PYnr2v71&1Cv%e?jsQw21y04;tmxq4dz36W) zYd$ye9`yf~q91tonvcGey9WJ2eIMqo-UW8_5$&Kkz!QN!VkG*AQ5dH=;ob!~(U2E| zeqt=vz1SN%RX_2uRNiAB)p8kq=0D=w!SNdJ*5;Xb_FlnU2^vp5@PXmvPg0j;qeuolDY;QH#;P8?hMqzbLsZ zn&+#NmUP9w7JD1>aHkOF2Gy7?-Z;3M<3RhkE<1Dz7&s)Hzu9{@`ugaYxlzWY^N9aB zxS1VeoIHnnj(8_G@87nKH}bM4|HruVc5hEM?(I%N+2GwoPeax#wsBosF{WlYn0TQ*jwj}hGA?aL8uKhkj#V~|FkYs+f)SjX3>=$a43ogTKwlhbE&Jun6T0h+ zF=lO0^uJbKCBm=&DS7qX&wBgvssQZI&YhcF!0{_%-(sY=9=TWW<2)nv;Q7t z9Q+h|CXP9FMH(kxMmvy+c5XBD4h2pl@0Hp1e|vS+r&)79G1eMrEBeaipL$<@8)Mmz z;pgpe%W^_Fmw(@j@$Be&_}}xl7_&YEcx7V_>iZJZ`^BjLi_i{aqaNRdHsnsU4|kxQ zSco=c!5Xw1q77l(nh_44-PQHgC!T&*$FZraH}7YNJN-c%mwgv=*DRCsk){k|U)bO; z-U`sCR&@JD)@TXDDOTy-gUYWFAI7p{`Pf zKibGkyU`f6=vCvMxv9q5*YUkwWZp16?SGl~_^vmmO@y1&eDj_snSN-C{m`#1f&ZJ~ z)*EHyZH6pwoRPN*GL}HbVz|wD+`MNo-0ZpDSi5-Ip8GaK?y6}|j{EuW?%NiPaHTzo zFjJNr?zOBA=vb+z4>`bhk}Z`zCv_~P){tSxmzBumf#baG7XB-LFGg)McXXBdKR? zLb+hM_C^?aXOPcycN*n~b*K?#9{Ij(GxTeny{|08eLROtZ!%MPKM!zpP_x+nfTM z-xE%Tf%zB4yHl)7@-Is=&8{sd+okAh%)>JQ5Bg8DZ$TN@JlR<5Liysi9etOUQuI}l zU{?oS_hQ4qy);Nyq=gCm7Ub<_$eOZi`?C{JzDq$H1{trQ9K8UXGCGm>&5$t{GD^|E zSnM!n)33QqPw2U0`ef8SI|1!N8EENBMiFosj+mAv6^?9#ZLzaI9Iv8o%68lAN<%mn zL%+%RJwFPvA!{|`gz&5c&h{8(eI)4)g5Bo;9tgk<$5!w^&T^3b)mAS9qS46jPFdp7L|Ulp-iDKJ^PoC z`yS%gd9hcdM>f(g+MFuF-N|pNO27GdxAK>wo-+Nc{M6s*^m`3`-@f!ixVHkYz&B8b z(Eu*OhO)r0F?|>|#@U8Gz*I}vQZT0MPa_#NC%(ZtjkX|cR{o|7q5O5|E4z@lLO#Q3 zNz)fT>&;t^u*D#)>_}TehV%TVy_rj5joC>EQ^$3C#xb0{1J!!}sr5VEv~_&<+*spM z(U(!S3~$dOr}uEGr|K$SKU zE8YC!THGW`Hy>Vu8>Uk!(rFRWX|4-to4n_~Sd90#9gahL&x~E| zMIEe4FtV@||&zZrU5wl&ji#w>>Sckt_d$S8Ii_bgmgoHc2TkyYd{PHuKU?}~7< zZ$-ZM?IZf)wHkHoLD+w%bkkF7jZyVa#Co543<$+-wD`jMz2OcIPbiDcv|ljthvQ^gZ5pJ zwH9F<2EQ%>&q03u9&YYd`4xK;^DDmPL*!R0{CEZVmBw+B$}h};W)4SM*pW{^Qu#Eg zFP|J~H$m=Mq|0xikxwIR_P%_&)3nzo*&@5nApcI9%%^yi2YU?W%%_XYAEYgLj{`n{ycuDOs=pt3^PC(zftHr5cjiX42~zE$$@Q%p;zZCL#=bJ#KD&B^#J$Ty_lN1$Co9!{J; zwZ0(!y*|GR<9{sP@amXGp9tGMpuB=5h79c^SihIq3qy$OBYdGu)(!ERBe zclJvtSM?}YIVe{j!rdj5g{hCc^xUsdu73Yw>xGoP8Ie=VZv@rZ-$+`4ExU!cJ?LH5cb7$!iPOqhrKy4>^`JB z>a0k2*5$dj=#a2SBP@$U!XDjq9$}x0bl-x0=Bij@)_K%nraR->+?VcUS*dvEm?p=P z?&M_|&oiCJ_F&xn8uE7w#x18|r`PvWSGsvsW>(YIZ*+%@8^F&#H~J`Vg`+2_omaa83?u?;6lw+gSu?yHVN?!sEo_ z!V`@r22U)WVR+*3498TULPWbym<3Zuug9H6e;CUE4v++QoXh1(`G|n}QxA5R+ zd>PLPyu6J^EiGPIRxy2LVND4xM^p=Ki;R`Hd$Fpr#y!X5=4*LL1>9+alnF4MXe40h zRe(|UMglS?DGdEG6xlQd;y={=ibzSA6m1yS4G*RJv8aQl9CDJv&@YUZ^f?GwIR8`R zbq@yb^!_DPuk4B!-4)hYXJ%Dxsb1NLnQC;5*^m_G@^$d5%`!aiQY z{1ewp{J4bq2LY$TstFLfk@RhVBrWA1& zm|v{uV*x1#$~waQPUOc7ydDKi16;1)4GEWD1VgSEum)3mSeSs0x%Z%atS>-fcWJ=mqO_A;ty3NXO4uPOh6RfoD2y)DS#NK z=1h{%vn@)7$1kC$0FdEXA)$x+2pOIn2|dt(itx;l&@%;);YpFulL!dWIao6(!V?e3 z@We{!aR4$rhJ+sOZ)A8rk6@PB!XAO9D|TpWYlM`PZ_fBTr@V@%tTG3|IiGNujh2gV%1 z`++eB@xE`&0le=U(}e$~3Hv9Qw%rq&CfM-bi1(cncH@2Lgk5;|PuPif|AcLL?@spP zy*v4Gvd#APCEZkp_{ z@0#+=l-*ODwjJp^ai7=m=_jVYJsohyi5YLtaM*Wd?#g^7(_ugIrM52}{gT7JZAsmd z9ZMYceM_2_>|YXPJCfU$8)Z9~dkFvga}VS)#nDzGGp+lpK0@Ykzm(p0aVO~(JVN0G zH%s@wQ~Xs5e^cRpg>O{&PKDp2@J5AyS;;$~@GFYmrttR^enR1|D!fDCH449|aLoUR z@ahAfW+*&feUNTZxLe^96`rB+F$|AsIP`Z6V^6uCOwnhNj)L!1_@|1Vt?++W_)>)* zR`_y-uU2@0!ZD{N%{Ts@9^IfM+L1?^qEL@;#(9xjNt)ZukfD)@r(OnQQ`Hz z60|i&oCW83#&KvM4k-7}fIb{{x*ze9!gap{bH0ME`yuQOkYAgxDqQyyHYi;8d$ucF z_e0puSezTHgYY>)_{AXnjUYT8!mRR&gXmug!pUppXZvQw@r@3^PY2<51mT8nP05;~ zstty(tm?r`-^FjlIj#65!yiDXBHeeN(Z?2zz;aCDqlHcmY;a z3P@RmmR@ODmEl|Ya7lF~*$ZoGN~&vp)wQ^rR`EdYbja_+%c=rU*{Ka<DaL#Bk$*LHD+APphy9`YS-2LsLgL zFiRC4g?B!bM>Ev@g6&EUlEyt^@mLec% zt_L)O?eB9c$CH(WojPI)SHbR+KiIh&I-Stx?K9T!nSHkMWw8By8OpI+IXtA|i+O$a zD`^6Ph`zVjx5qp}%Z~>GA(*Ln7$(eDV^e^1G!~Hk>Is1B(dk|azknJ`@zBsUA0`R< z{Ni3ffNL_r=S#pby>Gy?t4(xi{Ud|Ve}G>+ALDRcgfSCikDa@0M#D_#;+~E)4%flH z9rM(rr(nGW=D`nJWn#>`mUBo(MBRPCa_Q$#pQjqKu{f3tJ_bZJ&jlVhte}t zJ26Sp1KI{!j# zL|IDb9(c5MIR_>5900_pC1<~co+d!FUpf0E^gIJdcW6_DyPXnxb^y{HzfXXkZGd#= zm(WuSNOx6$^sih(PbnbX6-($T0HnJW5_*;bA};A!fNsD{KqPTahJ>CefJnrBg!qZ` zNr)#XJYLZW@$)zpPKaNQL*az@c`iUW>P>nlApJQjp@;ecrW@K$5q|0m5D!lqAj5w| zLeC*UhX0_1o&$j6;f{I(hD+-Yb}5{Y{_apXA@H1SfDDHp5Wk#F5_;AHG979qo>Qgl z%K<5$knW2WPDs2!;e_-r8<27q0^*nBmC!Q>ka7s=E>q!z#92Q`C#0My5>Iz4dqVOh zD4dXZyuu0Tj-E1pYJ4N~D6sNeKp927O+Nw1@Er$)eNMZCo}+;DpOA770z%)GegF^+ zyf_~QdeZbJ;8bDm14Lgfy%7-ly7b+ER4sBGhwpbf8|y^Coq)-JI{>K?EeCW1mIBiM zVhKG3fb^e`{x4TJA@PL@dL{H^0g|7P^h|{l0{4ssWI7=1!k>5v|7!Z8C|i6&l+C*^ z%2r$&WotK&n^8i86lH6(9mSb47j2*5|FX?*kFxEuKZF0H_ICUqfldk95c?tgpS5@5 zf2U)YBg%HbaS;C(92fD=d&i?}?>aj0zcXT2M3k*AVh8^H5!>*8FyavY_eUJS|GtPO zR0^7D$Xu&FWhWAW`jd7%G^uzvUf2CVth*EXhCD_8RN=ave1iOtsr#XStMGBKqj`t? zcu!XNa6sxWbUS*R#Emv3|1I+4(cw9++-v!75VwSn?x-)&{mfeUiSMv+QTPtM0_en9 zUuc|otonc-flfS6(f29(+X{aLxO&B#Vf-QpXMMNQ{}e>u9YjABgl7ih2#-~sJqSl# z3!u*q!s*uPep8V9`-AY6LHvfVaAjq6ZQqDc9(JvVtcB(H+^h2+eP1c5exRiIuJX#2 zh2=FE=%_9NhT64dYf5|%;8dQXs&&5mOA2wqVo6P(7QuQljXcaJv~v+UAhz^8v`-_I=^XSiJ%uHO#lFIl2g+)uP0uR#Rh2r+#T1HB7ZmrP$d1Xndhh2@2~A-)_jUswHL$@G~sr_COMyZ+$ul*_?GS6NZQBraT6 zTZ$RK@(t5wWXv3bv#N(gdJ+7EB2( zYMMhTNPKsHH7{pT?i~w#cYZZ@LEfUfbA@)~+VvyTW@iX}i91+7qNiG3r( zjRywbTqDcc5D44;zjU+%H0)>5q$nJHD)y`TbP_|=k8Gn57|Nrlc(l=MYw_{*6KFG- z{y~RBeXfKv6*Tc49HqhZ4|U4pM&+;y!uToT_YEoo8moBD{asc6a7e|Aj@Wl-1CDt@ z;{*cihaXn`14!(92!F`)=NP1+b*wuZ>tNC3`^A}^q#0NX%Q3a}UDN-1us`JJtI-6c z87-7O=N4%~=qb{EKsx(lG>8XHi2FIxen9%dLFhSy(DMeNuNZ`0JP4irS(<_3$9@aV zK=e(LJRp4v=qPLZ2vH1knBK$*fqUG_K2hPI2|fTp_k#()$N~ku3c3{}5x>X|$sgIK zV55S51q&4PD(F@aK)8>xF<6GN0m7{PP_|d>_v!jC=9w(`?c_(rt)pWY_EXuf)8>ES zh5b04j!P8I_|uS8^z#IU{wOSn>wNu3MK4zLGKDj}Xtv-Xzs|=~3g1K`j91C)E5{0- zBguwO;W-NbJs{;RRCr&9OLw2}srD5Mq(RQ6BESe;cB zm7_Nk)=gnaO+{IF%7g1?PMh9;45C$FbuoK?VV%`ru4u*plmQ4xA34P_eIR#w1|(dX zz`smA!C~OuscV|Ym^C9S{C9d?ZCQE$v4>&L=PqUI=9U@Ha{ms>!9eC>tfbV@4NQl^ z^>3A7n(9>cHXIF$n@!o~0EKxVM-zK$2i-{DMf z5)C#lQm4XquM!ro(oM@&$*ECPv!cp1RfR>h=!`y8R#EKp2~|QLy+g^fQEept#{rFUAYZ? zi1)D{6%MH1pJ~vyzw}w3K+t1pxHL1JPZZx>&K>z;Xrl$HDPg{;ke3`6Z#;NI#V!@%(M+Rc-nea*yvJDlac(T+W}T<>dZ;l5hlztTHbERo!W zH4_J$#(1-~jx}PXNIUm#cCFysK4Sr)yKlZzxOe?F&iosKEMlbpBOWxVPhoAeCb8uEpT-pVM z1!sKm?16i_KwFi(IQl$ftb)6bu{Jvmd%77$hW$L~*k=^uG^1k%3M=!1@js2cb0&DR ztZ`1pK9E97oDIYo{34Df(u(1C8OjgrYjJv<_i8sx1MKNmXMA~Qr$M$3i&OE=hkT}K zc;4tGf}gyPI7eA|bvbIZ%Xt@;qa?_}8k0PHkb9Em7?;ZATJ4=E0}WV@w2WoJKzRGg z2lh~tk9$fajpxGs3(5!1d`BF4mLI<52botqS5KDfQKDRyVQ` zS>3)P*w-0Y-k_u0*@?5>ao*WesV?Ww=i_A?yI z#CdMc2xt9cHfKG;{nko^eSxtB=aTtHMgXTjPV5Ouav7Iw*h@klYgz2}9TjK2ldleY zA&^Fg2jV%6aE*4DI2(*-+?(}`E8Wq)(U#DMcVMqA&z0A-6k}}tIlFlb`Sc{3F)@GT z#yM+E2(!~1b!_ZBoSA3tne#UG||o6h=V`{;U>3FZ_1WSwH3e~vlY4(yxfUSX_=6SQ`lFAHhFwk4pAX&(uD z>;((4p8)&lF!s}79~Z`cKI}(^v0n!JF=6ai!hT#B`)b&a4`aUt_7lR`KLz`n!q`6# z`?N6jKZgC(F!ry(etH=Dw_rajjQttd-yX*P53v7osJ(8(>+IfHpP~%O`w~X0IxhE< z`!Of0?U|oCfA#*3&%U+*WoJJ2ZR6a=t!9HPb9bN|Ekt<=q_KUY3_iMC7NAaQJ15GS ztUul!>`}oPxy~n@4p-N9s*ll5srndDrU!GQ>e@BAQDry4P48CYq;3<7aHdEp_Xirz z^ZeePvrlMoniFtDf|0Pu`TDcxu_rz?)&3mX1GEbhTDW&;KF=jKZ8(?M(X|roT>;V? zdvh8PU$%vBz4CEVRvON+xW#n-d^Ps$tiH1TR8CoGR_n*Uto=BHb&Yxb$Q|aew4Ab( zto^XV`Jibp!3||_KN!zN=2^tDZD(G{?-OvWpRnSnll<0289j_c63$6x*s!PfEwmxa zxW9K1?5E(o$n<_^ zHNy5G+G_H)n!{T@gnXXsgs?SiHln-sVn3S`dzjg0I1e7|X*-}(W?Lv z{}A~JGWwbj&{w9@v5L+QM>zLwik?O0_`tq4t-2L`5|J?RPgFjtc!v0&JqW#4(NTbD zLgLr1=!y^`pYvEW*?17t5ck|~LsJSO#&#j}P0B{;6GG_JOVV&&4nw*SdIkrKFbNWb z&=)Ft<^c5dgV6U6Lhn%Y;{(Xw@00}Qf4KbR!3i`e5`=`G`+X*09=8e*UgeZa=qUvh z>l-BW6aZq(oU=kgPZRvjfV+3$i0}m9Er16Bkrg=yB=j^X`ff${EBYoyuT}I?Md$tt zy3ba0FW^m(I|mSJ+0wZW1CdDQJ`DQn1jJh2^s@*o%4quAfQV~)1_FyUZ0T-5WNl8O zgq{RIhVv|h(4P(oJ&k}9VZRHI^X%&ZDZdsFMJ&Av@MgeFK)TBSq?}kl%6CfW=>{|D z=vRyVQ0PkwzIT=VI|@Il@J7Iikh2~z4RDIGPm$0Q56E!D0y2IV;Thw{dXoxx6p-{o zfb_Qs@K)fBfcWKbz8F6b=c`HICZWd%| zmoUE^kp6p>J?jbBA-M z(`M^%opr%3x-lA?JmQXH>&fLfKQ^x%7=Ca#mKcn@I1)+jX0a7*`XcxbjNe*HbbU{KKa2`co}iXTZN%)cmH ze}C4&mhyCa&SWF5+wV(?U$@u)ufp~Bsa*Nrf%b@o>l^4EbygVMham85K$>qTT({qU z2VVU8%mKVv@!<6kbK%}fpB2Pk6ol^y!v8hMzY{_9)j{~nLHukltp48+6rSTj{INmw z{~bif%|L|}h1J+!p*wo2U+3d|cV$I+KSlHa3#+ROVHLdSqNrNj2-Lr-!n*Q8?aJ`2 zQKCcrs;De1Dk`brs*K8YwZ6*xeJd;1RTKl4Zhif$G5Ys(s7s5mrDF~DaI9HZfz2U+ z;$O$6c-@*c8+-KLk+H(#MDMr;R7YU znzDy2`%SF*{1BGOT1aIz=)m(zrZNKMhl*Jret&fdh}IygD@PT7VR8Sw35EcL{TSaZ z3ud$jYKWoHnsqD1x`-jIj*xBx*Epc2{2A*RGNw&;3lp^8zifXCojiid{s^+Pv*rr8O1 zLO1P&UYI(U0*vXU?nT0BQ!}PcN2kteVkHs~H(DwC0q4clcxOO*qVhz=CqzEuizYsY zY=C0@DPW{m;*o9z0Z5P3^V^$&Tl-0%iGIr@GQw=Y!+wm$YltJwb^a~{&hM@cAI1&> zx5k%r9lsTd4j_%*v!_~*^0JlH_$>KUwZpo(3bBz-Hu@pd!c}3YynYT1Ltl%fWvL6u zxDrawx5`yf2nps@XUviffcPJ5f7tYCGZlMKd!bjMVW*;QGZ@jobRAT-LnfDQH#E-r z!slAs4UU^=bi0v(ah&KohqN14-zTQ!#7l8T2l}uS-3ECfg%tLKwHqAQ(Rjr_<{6c| z6aDf6#17VObikb(vgnBI1`{h9pb@YM(AsXmD}xW~0Gx?71mK!X@9Q?q4LYH>bwU3h zjXQ5_4XCr0dC42QUv(J=IUmHipaO@twIj-C&Bj@NO}%rP=NjfEoSBxl_kH8!89R0T z2`xM~41w6yWY5on-fu*c{l={3QTbUZ$=NYYqwqday-&pZ4D~((?+euX0=(a=-tWbG zk$Nw}dyRUpxiPE8W}e(-GaEMl$++ai`D&+dcJ-$(APjhPcyw5t2-D^X#--OW$Fkiv z`WVhUyu|aZN~0fn>KyoahE^BC?#8`q1&(|Hn4Db5GG z1^31+!MRF^o8X~;rc>%9JM4H@=PWmE%@3KUJq^9Ffp+=I#>GhUMbMRJ#$KJ8L4uo;AE~6(QweFXYPdumt(N82P>k^C~D4_P%q(-_qsPjxxU8 zHb#`)7jRanD7)}Wm)#4PpY)^rCgX7@x~J-L%rLPWn@ErUKzvy@2WljCqA@{xs9Lpt zF51(SqG3W)6hfyy3vEz(EgPqNUz1RUY$ zGhYwFIiY5y^WBQ~brvx2ltZ$>n{zFn^^n_ntz#iIT&u+>dJTrs7=l@;dZg4x_N}k1 z_LX6GVo0Avukh#gMW6B=VMae?rW&XWsh=`)MrQc+Un?uuTNeMX*K6VRGJV~5`AhpP zohUpsy5FL4TJu4c3NzI44$tkO(fyVaD8$s2p$7R0v|&G^qNY}($cDT-mZN3RM$6It z7V5{ZW(MoGG^ucTQ6_12LLTFmjgGMBR|R#42J5%H19y#ZPjRe^+&7*Kh%>egH~g~p zTUaId&>!g11r6Yu4DZeS0tLG|y6(>s!K|ZMYZTZGCSc{8nd_8@qzn%&?EtfxeQd{X?H3c#TZ&0)`#y zur^}Nh23oE1V7implDzX3Fk`$`X)%6U4HCuH!xQkE_})O4qn-~5Z{mm z&5$qGC_x_bph2D$KdQf97Vd0E%Q~Rm1vd0SBJeGWggo5Au+fQd+K|Q$^a1VYbJ%dV zfQh?pg4eJmcC#;Z8t1~E!QD3uGa9X#rV0n+a|U$%jc)6UPw00s8Q;Y#8?)j6T?ogW z_$Jo+HaX8rRdvw245N z9fPJ#V_({Y_90D?HvQ-4>ofD)<{J%6o1v#msBEuF7m;`9_x=B|EG)p;z$;}xeI)fK zEDy=5Eby+9!SerJxX1oN+(~7Kdcr#M-<|ioui;gd|6Hf`KI+3+)C=Zg_&RZwyhq&; zdZU4$(k~fU&n5V5{->m#3BCR^v{K=p?DnP^XghAk}ph@cwA?g8lR7t$!fHi`BB zb6$t$0v>$gLj2EGE|h*KgwA<18oI~#F@#>MIs^mx&;BpXK>p_`hm#~*NccI=Omh}b zIR96u?>O2Znh^KJgV1?iD9u3bdEVgF=$i(iZySWZV-Wgs&^h0p4TynD`9cZvvjDL^ zIz0vOM!*Y5C(=75%zp{x_YxK{n=sYhK zeEFQur+;oh@^v6H=$`ZTq#p;Qe@6i6-ysP-2LS2cGl1mZC1F0x4doO7x&b*pqQ5zk zUY;#s{v1I1I|UFrOEJGsITw)A^!Hsr`gTtzj>FHh4}bKR<2dryNtn-iN;!Fe z^pEQbNcSpw79i!N0OFT3NkWeUkbJZR%)f}jNcryo(*A7;^A7>?e1v^~JeOh{ApI$p z^zs4;^OpmXF9(qEW4m)bAi^f(Aq>JFH{uO1BUz&eJMc%?reLFjegz8@^eX69&`_`g za`773reLFjegz8@^eX695J0$d*b9nF9d;bR?6ALWzGJ>?I_!SiHd`GAuRQUf9j6`a zvo!&$v+uC)#JL7?KzqXew*4IpYnF-!ePG<<*>gS)e&_T@QAa`dDIEFuAQ!3SlV#Z)$VfGJ6#uC7Xgn(w?`k3 zHc=0)<7Kw191rVp%O4$q_-W!O(|R1j`ar$=5k?p$3h%^q`|}j|iRMc3{7 z3E0uUVx$wz_wW$c?Jv(SAg)c7!gc%pG~#O^8|Q=YD9E+ae}`~b@z;a!p9Q%u38G&K zqW>TW&qDmH@>P4MgAc{I(nkz3ZueDIl~njj3TrS~KSUST#JmQx^_Wv$xuLeC1}7F` z-`@~@sVTu)f!ft28-&v#JF8l^vK*%%T&uH{CHF&bQnBXRJgcazD3Vbda`clsg=IK6Vu;Dz$FElk1gW(KD@Hm2rv@m)26NR{_Az96VGsP^eNEYFDBT zF%v@cb+X>;C=EH;15&Whm8`4b{kdi@LL7vRyV8K_;w=sUsVObQ23DInLl@f)kSi?DJannH0>2{_yh-J>E5&%ax&f=FZt&C+Gh5 z;is%RH;&~oR_oJob|#rU9TUxl(=VvAGnL+`GihGPy^n>^Nxkwq_p|ixDZ5-L_sMlY zzh`2u%!bFV_7&3ZG-TWQbZNk$QwYFc0A2AHY%v<%2mV`xMeCrn4npWfw;qM=^hsD|EB#scnBa2Fuzf*9A)-BRFmmjOt5Zi$yCD*Jdqt|#ZXj<6ek6TS z^run5oq+U*<2U-l0ziKj0wPZ78OnZ&viAbgOSggm0(aPPeT~DOw|s@ezT4J_F_Xi7 z!uGc99l%ce1^Y$SShT>gbv(oPay;W!DYpo?wZGm4`NVZTY*4sP_ZvaOug{>M1>hXN zSm`eX;iOsVw+6W{2%BK2Yf1EDHaS+=ZH2hWSuDef$mE@0M}%=zR0?+i2Vr&*wu)pi~D`~-X_m# z#u_ELCPlAB>9F&=J~$6$xN&YX`S}h#+uDw(J>!DqU9S9R%m+!}k5 z^JNTEBf?3y9WpEiZhS{>8b{FHN&wv=nK6rjQxX;NueXVn> zrzf(nGuYgD;M_FRJ8mn~joDhZ$Y%==^I4xg!9G%;Jb4C- zRVK>g5-UH~K3dm3hKnaURF&ulx1yS(&y7B-%iDT7#@>p)Vyj!NxnsW(^TiFezBPbi zKm4wasQIl0SZ5G!zJ9M*PaxL-2CV-ZLYR8LwBP-n1DVHoET{Lgh9JR6GAT?gkCiWef=PG{~&aRKZWB-K=h^3vjCCT z<(U%ZPXWYOGJPyyDxd+F2KY7xwZxBeaEl-3uaLeu`y}-21f(3!BLU`D0aE@_WuK$$ z7Xng#1|a3dD|)P=e}aUiySD);?}UW;oX@7agMf6m8<62DmGtsr3G;IR>3)i`Pf_*> zfOHoN$nbOin&E#(LJ#$3^zWdA`MUurXOpsDuk1MwMmYt5l*4&y%E^+@GYOD#+!E$@ zqXgUnKF(X;1lX?dLx48}-v>Auunv&^l>*Xzv4r_606A~F6p(y#6g^APslVX7@MQ#o zeD45G1UwFy4A=%Z36Q$G34o1&od2c%A`x(#!V45W36So`0wx2-0%8m#&h@AJ3nM&M!ZrmP74$1uprBVl zw}OU(9gvUL$TkHV74$1uprBVlw}Jq|ox{#k@GoG-_>kj>qYbb-@^Yk~bHkBGFjL$Z zxi7K_u*tdKdB7QEdnf8$%oHDtk~7CATyMMH!QAj!XQ%T5W{!_XpNM`t8Z*ezJEM02 z9*jK{dn6V!)-g>nu#MRnvx@^Am`Ny?R=tZ!9BHD*apQw<>WjrZGU_MI-=n-y53@s} z2F|n-xNeWW2U(=+_Wh@duD=Hf3fJvRCghM`o6QQ>?N_hj*WZJD<-cx!mn(kVUQ-n1 zy^C)9qiUqI+RS^Bx}3b^DqP zxr!v{nc%nL+k^0L2JurbX5}}7@Z2E$z99E+2H_6`@jH;eR{3`Yh36PTZKeNL5S{Id zl|Ctmzbc6Sr68Q`nAQCwK{(qFE1m6#75`Nbf1e7-s)zB(DJEP1fUSSLBcMp0aIbNt zVhV7At=JD1mY3mFqsp?1ezjTvj}ptBujOjrdpJ>;My@U?URP97EFA=@-NHyz3=imo z_-A0t`c)jQt0=?lww~M8u6PpDRUC0f?JDG;An;V*8g8!kRaO`Cz5?wFlBu!+6aQru zK5W!}pt^D$_TC52F)O@pce{2h4~vqa51(;^bErI!^Q_^8T#crj%hQ5Yu@H!Ln}$?T z>QEyZI4hyDqO6D-$G)-@V5@mW_?gt^SC{&a@AoQH!N}x_8 zQ40IREK6meI#$gvYu~F__7Gprs9)84>o$M!t|bU0T%ZOdsL*obzb;DRT}u>5IEjLg z(_c8Nrd3T7r#%GSsQ|q%#|1Rks`s5CZi9r{H>}>bx{P;E)Yev_eUZxFaC+bMY{1;~ zeen6<^=O}Io>7gS#;2*oQ$=$DH9GU$hj^KY_{239>S1@oal)YXo}bFGHDDZL-}w3wT; z=u)qvE_E?<401ojU#gR|>P|bZqHpbh{^Ng&9`*l5m8H()aC`R$nqSPm%G#{+Vly%dn+eqB$$ru@_Og6T^4x*nm72wc}o zJs#He;5ii@T`&JZ$=CJ#14Y;Mg6ongU)Qs4C101@9!2kfJvxiAbH;b(Mo?kh#tHyntC!T|8>YeE1n!gzYv5!5G4QZApEwV@Qe?_nNL># zuG%LKH@>uX5rfC2);?_jAI1QG>0mTKp6X!_G;j^z9m41}fM3d1XBS1XG#dH~;01r23jIbgP5ZFp#P6covD zBf)^ct4N093lgPAh*saiiPC)5QS)Ckk{ry#s}C^)f;gmc=3xF^eE=HZUjS>+5L7QW z;8R0b`VXf=+Z%5D+0F*cjR$71bw^`W zM~Ul%Xf&?JpJ-l%8S3~`pPx#3XiCLmM9f>T!{#T@2J3{>@m$g-czv^$Gfz1f>(VYI z6tXQ=gRMK#=ZrQgHwBQ#xGl%P6H`Vs0U`$-&%#}{a>TxRJ8<@moLI+{fw%+hTX#hH zeDEm~Ynw^CRuk?{5GU?VNop|G&cpdpP6yuqx3Ts#_O?5n(*7~N$9iX@Ke-e4!!&R| zHOEb#WB&X!&P4bOYlS9YjKlTO2+yu)W7HD#6)&B)KRNEa%{c7G{Uv)xa;!B*(D7kB z2>FnafICb2*Fg8*s}cSVkuM(jC!9ZE;v51S=Jxx~ZQ#Cs&f{}F|FOUEW+4na@iZ_@ z438W41)#suvKMz+1k=0@dB?_H+-ng`E5JEcT#vjLcUuJ0{5Y$qn>5^S5lm|o_sz(4 zDlWTo$$6V|$uT@r@Lqp?Oxmhx(P@hoxze15b4f~}GY#b@!HM-F30U)8iu97{BG>7B z_O_$qi0*+eG_tTdOPj`VHtQ8Yt!42PAmglr{0cqp}mRsSCI}Fd+dA}Wq^Czb^3ad zzJG)t9rqdyuOgq39=jTmt|E^YNZbwl3-COq);<)#e}^&d7|L=&hkcwV7iU2?kOrra zw%PVE$95ae`nrGQ8Bqyc=!fPd;qHd_c~+Qx^paGh*W~2(XMcCY{8T4o{;|kt_~S&Q zxx;=v%H&v_?Qk9T-b=Ztk(rd+nH0M2`_*WkZ?Onzn2of&3u$^M()JFlZCi*t9Hd@{ zWnHeLPZZ&dgRBQ_iS>2xw|$I}Rf@Z5oaUHg=dfOWF~Ws5FX4E!(EvNx#~%C49^3MJ zdrV7gq;c@QM~qAD5k}rGaQ4+LX56vOxHqSA+LO<|WDc({UtN*)E2PUWT}DIY>NQzA zl82|gXU23rhBZp?b6LA#yv#N872w-3FD`8=?65!i;I~J6pZejd%B)}6jM+&z3#}1p z_3iXKPYJt7WA-bM;m0{uyrZWL>G!QWa!$3bT9fsQXk#|UIGIVqj7zAKc{?5%d2j3D z@~q>?!(YT1vrpiRs^f7+!!H5HAkG~~V^ifH)AR2??LEl2zlSreCVbkP_a6LbT76%g zZM7ZYOC9EDIsdWw<5gdM`VXm({QSApEAvmKd5i|$!!bYk&M(U3`3`S!jSA+-ME)mT zG1k`MtgJD;8k@eXst#Tl5N?AGX zzLII$fit_7K<+JNtFvB8fgRG)OFPIHc2i)7bi|!Oc}w7ru*-nm2&66SvbBGi#_Xmn z<3|PEy?MCn)E~>GT@P%NmrI^3v5=ukij7 zW z_?^%)@QqqqVB2%wm5u#pxODp#qWLsA(7wbqUw;Ps}OMMjiN+t9Z0Mh+(WuK?) z7b^QX%05fkrzm^3vQJd@vC5w5k6#Y|@bg?mdQpBSAnnge=xGE*5Yzn%766jZ3rM~> z5_14xxOo(y!P?f|K@YX%#Z1 zU0Yqwoh)kHsII7&BGl=8S=g^Seda7*IcABi<6=3Rto`7o9UqQuF9esi4i zW(4l2MY_>k>%8c+X#?E3Ad;#__0OyE0O?%jU%-j?Z^*dlrt88j3bN=nm`5`N0MnQ93lshQ)zPWyMB zt&28#tZV)Q|gvfW$UzA ziWg+L${KCkKQ^a6KOB9xVr4r66O#1&9@7lu9({h6)AVu$Qvi{N=>>rBFVd@^TR{L} zUkYgb4y+^&-*vg01f1=(Hn|Gd;lVdu&>3%<+wqWJ$A{k)i*xaA#e*-{(9Mo$vZ_lS zz|{!-?U=m$BiMG37SFmX@6Tw5X6bGJLHqKmR-$E`o-W&D-3o=*kBPR#jf8;Vcd1jn zh5uteh~^oUNE-ih+adO&v^>6Yj~e<}*W3=RS908R3lk4HY=_zaRZta&Ad5Sk+1@bR zC{cJYO9?X-56d|FiG*}SzITALEvkd!>I7i8c8UJbPbZ#hHJ2-YivD`|`%`;gclOcm-|OHdH8}gb8$3POWAX~_W$uLjY&6=$S!jo@!#MQ|j5*q)+wSYU6?fi7-O=ififA=v zw0Yb!{2l}QYTQxwR@1()HQDZHHE^zX#}f|V^MTs{u}5bs_BTQH>7N)4TOd<}f%pQs zSLkoI8kgqJ_%7Pn(e>KyW$bzXb)?boh5d$>S8SuYcvkg1jFE0YxW?hRFUn}hj`{Bk zQ^ynY5fAKjd17Ac3WOOtAQ=a1I;uF>lrBNUQN@Yw&qTF(<|n!<&P4e=XOIr_6O${> z*!`Z<_BPLMgny5Hbk`!Bk4b+@Z8scCdd#@wf-Kv#?MRCo>QBEe$B2L1wei$u`>3uW zjJ26}<~W&#OQANc1c((UkAJ2Av2O#bJfV5}-o&0RaX@3Eb_Ujca z1*E%4fb`ETVLkxsGggYbC#C4cZgQ}rHk zEnXeYW0kIEv-J0F>HH-N@0{+tH+vqs_yI@KAYw4+KVQ?i+_Pp)SI5-^f43gu%%_^) z2^?AUf5pOo$J&_zZiZ&?d7e%-`7oU-(=?7UD8317$7zPzpDu`%{FH}cXPm&tV*;&0 z`_osQ=h1Rd=7q^t;Z$fy$7`_ud8rDQpMio|3VDoM2Nh1jCQ#%+{d4_Y^yA~40e2L~ z@8SXAlPL!Z^(F+``dvi5X5AKR7Rd`e%C#GHCTz#%jXxIq|Iqd;_>_QXFA3XX)&4*= zXCira3Yr1^n!w$S61TOzC2&*W+TN>hqe<{_kLMQ;*r4CDL%-*MZoU6}@NVe#dZ63G zN*n0+>|*YS^GMXoA(FfF_qi83<*hkar5B*vK2wA9ZJ_Hri}?nv z@2A_%wlQ6eXvaU8xFD7D1Cua6@Z0OWhcSoK!?~x^&;_1>F3^s+YkeTnnc2Ck9qEO3 z^)TbC&lmsfYYP#F1?XSQ@7qV}GTcAwqD&MkOknl=aU6bK<_<=7M4G-Cz*8uAXotQ=g6oP*({lkl zD+LejA_R|2Qy1!ffBF;&9@(rYX_^|qQ!IFB2VEn0WSTAt;3*M2v^y_&WSW)* z@Z2wWXm?uh$TZy^!1I9Mq1`)zN2Y0W08gplp&fLS2#-wDHv@Re1P|@r5IizX&js+T z55A9wOJTgre1@Np9JhXdW@W?bR3*e~` zJhc0s;E`#%J%FcD@X&6j;E`$C9Kcg0cxd;8;E`$iW&qDx!9zRflaYopP0t1JR0|&3 zVQnFJWSY7ntm#uDcxYEHcx0NU2JqAh9@-TN9+{?#0(jO59@^a}cx0NE1@Js5cxZQz z;E`#%J%HyS!9zRrn-ON2rp*C7>je+(pvwl2Ow%_5cs2+g+M$0A9+{@+0(c%4JhV#} zJTgsPIM-~E`ZlFr-Y9rzH%aiwG))cQ*(7*qhxN$tOQz|f0G>w#5AD#+0gp`6vH+e( z1rP09f@j^SRGabgb_);Udx?1Ji`&OBj84oQJ14*StjjnwlJkmAjQ6(VTkCYaIS%6< z@qKk&dGOSB%#$}; zGcNQ~2a6KQU&m*`j^$Gmu-$@llc zBl|n@{(*l*Kaad({!{VRh+`Yj-Xi@;v-W|)-I?|(`Z4;RjEvLs_ZlbPN8j}<@*QF7%WtL^)29q~ zqy?uB*7l3^*^B<~=^}ZT%=-)IUe0TGKzH8s`Zo@pyTN-Hbz^lQ_|8q7m&$ijyw4Z! z^86>c=Bn4;e>RFW@AaB2O?RW87i>4_Hnb2HgT6D38ZAkL|H@v&5ITl?!VKh|;~$!V z=x#wBn4U5SontZ@HQWmc-yBIE(Eo*l&~pZ%v!Kun6u%XcJRrSz5IVkd!VDz8b`bg| zMSm9%a{>;9_JQT5e053>Leh`xBWdEnhw*cW`zkb$GzkOHw{c($L;t}N;(iJ;4rL_$ z0{Gm32LKU>bUz@*jOkT?&`_6`OPF5(I0^Pk0Ve{^0pxy_48UZ-4upsN?@E}DbzEZK z%MN9ac`VZN%K<4rTiGvE_8EW-j~kG3696gq6Ql$5gz24tSi_ah+4>29ZGa5#4nX=_ zCt>~+M3!zR+(B--feh^pn!&nKo#I>2E_=}arw*g7l^_Kfyi0gXJ^IC}Odex=y70UfLl>3DW zXTFesj>37a0dZY#f3NtF=fdn%{JPyt1uyyCioQ+Zy56~!JYA1K7W}&2eNFMFDEBN! zlt)PO6TH)nHryv<#q*IqR=g*O|CJzqo~LBx|5*@!cM$(qgZPgG@s|hD|1*ewE(jme zN(^Wkd69J4{r#6ATy5b7*sE}5V_E;D23O;*tg0z5TZ6?M)>{y+<^T&%uFZj5?J-pG zRfRP*4;9PMRN=}5EFdXdQ!})5!lIYJy);*g2a@TUnERIC_1t>;-eu5?@Rm|EUGH2Eqf3Pl!oA}FAJ{CS)XrM;8apwSaj|F1vm^d5j1$%!(8He z?eju2W=s#i@8@a-U9A5KfBpoT^=n#ZKnT-97aApV}kSzOYB7Wp94@IVaZ3*Fpcm_0vtfUmW8?1VGH8ppT>GHJ&?T= ze%uYeaNk*hldM9%ozcQ!dwkK0DVO z-Sqw{a39`wcXUa*hr{v-4%moZM}J|pgBVSL(p zaJI18>p`4*2Z(d6#%X&Z)_&j6@`C-o?hkJD_LO$_9^@Kv+#%d^u?U)h>Jg8vtm7(Ltz@L89m`NJfm=l8cp zHD5yfE@B?*GS*c5{?VA{I_E|idkx2M;P>v|PwUV4Z3 zP~9EIp;oN*-wS;U%6EbhvANYY?6v*wh`Fs3hHcru81qcLqx^cr_Y0)kC)h9Zu{|F9 zyb|Hph?X|nxcbwO&Al~D^Og3*`VQdSZ}UFp)%x8~g!$te#2%dlDLd5NTzf*-8SL3<+Pb{kS@uwO%MUg*$C%MimOe7- zxfGYPzRVm~pNhOlMVc}%HruiPVA}F8&cT1CPZ7#g)7GzbJ8z7sKlf60b5ol0Ioj*A zm^6BG>rFR$Tm4vnvKQ-4UQ3)%@qXgO3Tqne#T~s&zZj(H6w`aS-i%p%3(|Hm(w6BL zE?t^#-q6hO#+WV<)@GE=aN$itc$@9ZyKU1JAe)cyegU+P{^`!6N*-Q*~vfkUgu1-*BcR!qU>&NogZuBOkSgP!APvd@`c{r zLR~EP1#(^1gWa1?J#_ZrQ)3OY!D(M{s4UH!nqoQ^bI+Y~!HPrJd$YJS67O3{nZN33T@j>Fy@FBkc}JOQSnQ zn@V>+(C(D9>2&9l?q<;)X?IE5?R2MTU*;Y{sTVB3`NJ#Y=Cy8#GjSHN@qgHR7x1X6 zbN_$OCCMa#AvYjE4B?_&6v|aWiB2XL0^)#}s!%b3a0v~{UF%)< z+q>3!*V?+r<$Iw;x>-{Wr#o5fBGTPK*-5(7#V#V?GYo#V#V<*C;zl_jO_yk?tFmT|~N9IXkbm)9!4~I(wX^@8wM1 z2B%|PCTDwIL06#Rv-7MQw>*C;Yx}3izp|slRla_SIeGn@1^#~R`{wtHYgf=OWl&+i zgp{IwoV$B8-aWeCDXl}YfBM0(w}4ykI5_qh&6(Ud?%>!{T3qsHBM*+PbfhMiV_&JI zCRby>UUMbieUq5GlLuc5zau4iANB{el;nO>4vrn_h)>qBf5wrRoPzy&ha=fDI6TQ~uwU;;O71a2%sV9CjeR9(xlc~^V4seA34f^5nXF^~4Dqnsw@>yHzgx5< zGyHU?D|rp}>xqXI|8~jc*jEyN34f>-pWI0NPH7Gm|Agdy*dNr~$#Oj>|WXJ0#9I;k6BK2O(3uOiNI#er)9MZXtY~r zc1B3|THg71Kcssmn9 z^mawB#Zuf21M$*a$3y-y*YWUjz+u7?lyxLQA1F?86apswFc`W1H9^sKI;SeyPSvQFK}qExeKZ&H8w)X6cup>K$>w-0&!Gn}C-GB95%5e%^ z9(%rI)6^An?m-cn&&AG{j;oq3{)t;Tza_I5T0oV6{ak3iWcym@(EkO^>^tx*__Do%}Cm6n;V3=m=f!&5*x8mownIDy~-x+Vt zkAzqJ2=Q%|3eL^1;@oouXXcp?jI7|iJbMHqD>y5^iZj#|+z+rS4eSJV0=t4;!E7)a z>A6nx{W|)HiScZ1goEPdU_HJmbziA(xyRj$J=os!sL{*Uji}{aAIe zuSPy-oJDtoZqNgIz;<9eFa=BjGr$Zm6U+p&z$`Ea%mI6Yy}`akaS!f2}vg4qqGfrsemXtiGnD8Q*_2`!$U^XX@PA zNCT2b^_4C9;s|qX4L0d9LZ8AMg@Cw^Un$}8v!5A?R{5_LDB8}~r9u;D zdmNq#E&FlyQ>bXWoL+Mey7yf)Dhh+p({RRZ>NK{;Ca zQ>7d%!XJt1Amn~t> z;fuM1Pn$N+a}w5oGU6mgK7~070n@x4mXY#?d7le@ALl2}QGSu~#H)(7)4M>?cKX8d zf^v`dEynNdQS{^}IxmXe7)4*~_}^j-vBnlPc(<^LlmuE>e62Xpf>U|h){q=Au32Mi z>#?_`cgO;|XK)v?Fn8RdLwLcT7& zsJw#DbFh>)f8Lz?1`i#izC1x-JkjH@j3JApuygqFpzHXx_Zy_BL1&}5;j!ue>^PjF zTAcVNRc||uppSTbiQ};Ck6mfE6Hgh3LqsfA+ihzc9*7&}7g`RE`hpTC(FZ7o7JQS& zx*x+>@1lfC*btVMNY-0j8~owegDSHil_*{ha&H|35r zS>uwsISDU(*W>o(U7M_a)6KqZ+*7Pgyi52SwmUn2Je%%K_U)elk2yJ0pJ>reQ0{lM z?mn>AZ0zrsdgwnNU#6YdIAqm7e({sXyPk#b*mBmx*yFSKt-BGpYhdDEF*kMSK_|c0 z*xTlgM{DhDIrkV~CUId7bInxVB~1Hn1##z4_jh(-t!|k!^X(dE=IT0r;?EKM>21xC zH%R&%#7q1NiNB^lR=h1*jT6{8axA{>%O|(G17IrqtQvlPwfGM5_&enFWb%9xd4D_W zf4A+REKE74t-LsN_Wa&!asL~o*I<9A;s|AQ*wiapGEPOWk^fy7e4&>vQayh@ZCHT9>tT z4kR6Q{OXr!>&m4*^|JEM0iXD1^S$zrcEUwIZ+3QmyNbQQSNRG55%|OPhIdIPT}wt} znk5Xo?n$}CN`DS_N=SW+jPsAbJiphDpB*=+s+UKo6Lvkw_h~1lksp$`A~#E2=wa1^ z=I^$0&d*%;K{rO>qb3rlJfa61YyV5k?t>l&_Wq$~y3v6Zoj&zPZy)B?eYx&6c zd*q?c$qn4h;6y|s2H_n{|WwMH{f+J4SWm~d1(zu-#cfOg;V7IfB8O!%m)Q; z2U!mrFbvE9dxPS>ouWlvL)IA}>t?1u7EXBp6!&vL`u_p9gUE~KeTA}4)Ysyh)7!!+ zr^#2&>I^sxVl?1+(3a=K-#eheDI(X2zr~=eLyZDuUFs@jo&!pJW!w_J(`2^9I|Q2`KRz0ZMqoESwSo@hZMnVU5Bng=GqT3Ud?!raQN5*oaXm=sQEs z!(2kR#l7AAG}jNVidz%6HV#Z!ldx9CdRcdils_L8nx8!m7x5k`e|`%$LfiHBkBYYI znS5u# zDf#^(nE7HwxfO$Ovc5LTQ_G|L<{NHP=a$-Hb?G9}QMa##nK9SWH;+^awr+G(F^ic{ zb^Q#PJ$GJ&=5tKmi@%KKBJ#ql64PfaZ21Adugi|s%&oJdbar`*xs6?W%ycV^`tI81<>Nb@vmC%j!j1-Cr2{J0ES-^T9)h zw=v(36(Wo7|4n);gQW8C*!A3||FiX6|d;T_u+xR zJUM2~J12Ln`Sj#nj>Nb6jr-xdJKoVx?)c=Lll^}5my-`;Ht+Zk-u)3~-x_C}{Okvx zoV?r7;jLfdcAobaCx7|$?@xX<_tTSWF#FKq__zvP{_)4Z|LCA8&-6A+iKs(!QnGxU2M==av4JN!y}?VhKZ-@Fp1-NSlhNqd(z zoc=T8m5$mypNM%X-fn-1m=J(&}Ozv)^0!`QO0zmiR}x^7FiGcJ-B z5|yf;K-|IJxytN&IF(eAHpH_fs|jDE3&Y+|`!-F1lC&@`7* z!{1wKqaPkJqK)q@&6`#^Z_bQ`Gur5PFLiy@9#CccHXr&WdH%EgZjH(>@lP^)BN38* zj&Q03?B_!JoVGt@%E1V=zM4tIR1hu7vaNpCUSE}a>igoCI1+i|c4(16ni%K@VHm3) zmT)9LWv}8=pVoVAww|CPtnVedU$XDyRBJgEPWl^k-YT4$sRx(HI%Q%fdR@00>wF$-EfuU|J=YTZ-l zUI`ZO6HarTmVDYEch_pHo7G_^bjapmT`bHGPaU*n9}ynrUmsq)a$}(xPOtcN^@Jhc z=CJ&W53jR5NBo=DBCU8I>%%>_C;nyBv01F!f;F69IOa@W{n0kZx*X__8XfC;fD}IW zz&h5ga%$Z1`<>Qnm5eXa*O0H=@7nUaO6TUiVe6G|XuCX8qV5sy)^vlWeNm}7PCSvI zl@x2P)t(cjBf5WIyQeR-+#`I3^;x;sP}XNb{*mnW>N!%W_5 zp1?h{yl=0(ZG^|sW#abA&LiaBU?bzwq<8ZrV!Eq-`jxWX6#=p)z#@3Q!f5|5t+K;Tm%6zVda*^jeKbgs zi=8|OA*Q`uPYyyyc&BZFh@?~SJCZ&U#V=z}r2X|#?*A4=|3?(PAd0Sv^6$@4_RmGp z^h+(m$GQbQDT-glUE#5x;ZgRzqv+p9(bBH?3qL>QEt3A){e5dWref;w!9z-|B|cll zZQ(q%)Mgt*ZtUC3S+i%ciaGszQx{CH95Q_P;E~(_jZf32E-bH*nfUyvvssU`J!qxd z7rjt-F}}(wS9M`+XD6(cLV|4(R(asEhFMb= z&fwF)3uMifHOg``zqr0(`n}~bKL8WXHZ$K#%hj@=?T}+Jtv{@4ha3Ks>5#DSZC$2P z$|q#(<&%gMwUENwJijwC_Ilsok;7u&hs`AwvN)?w4^hFI*FdDQMz8mY{J2OeZVo@5 z3$6FrBaQUC=A%X)`ZiN0r<*XJi(T(~N%dFapSTryoN?olSqD)R&%B z#mSDEX@4Pkd<5=t;1oxU2ion>j3e6n;94-2JTAPF9jyjq%r5lEzIXJVH{nI-GhEj6 zYNEEsKF?F;_UWMnO)uvAG273N+HnIiSvmIsZfejD$oUhIH^Mp(t|=op&yl*_b=im= z=~G7Z?NOY(J)QSn>b)25L)H6G-t*OaKJT}v_gi=`SMTM#SE~2Q%SKc>9S7um>5{)` zCp>(2;UM2;Jo5x`;Ah9jj*AC9j3qreJBe)iu(RXaH*hy|Iqwxo-+%NY`2WlukRKEG zoE5%vWh=CE=J{#PmM`I6Y&zJI9o)4`phcNvHT@=8%EXD>YstJ- zs$43}a=C6tDdn;p+4vtw>)GYny1s}@A-;K0-@&hM>EzR&%+Axbtlq&MS(gX>Syu$h zv-$+9koV=>@^;r9s@$?Dw>wn1@!m_l_u_r1dLPPrzIxB+{TB6p3-9IXy`1+-^7ji znewyX)A%d=+bx_T`iyvP%D-Y>rp#{##r-&ihlz{07x`3B`Z(c7mN&!A0b#|;Lbod; zGt2FgUGQV9zYjMu_IN&Wu-evW zwSN~ip0}~CCKakeG9-r|&xOYE8k4JeKf|V}BT`T7G4rCoXOV*&c~%hz83$$FW|q$- zuB+vUCp=Z=fAk;mM5>Uifk_&9#QX5m);KOqQl}^IBI&6GWsVkW+!lYO4x8}>FX>^7 zG1qdr*T-7Zt$I|@VU0b~oXkgL2c8yj7XxK(V=MaGK z!W@NwX}%qdY`>d@CN?>sJXi9I+#kG0(RTQfcVchH_a;T#>01XaJmJR{h+DPSHZ>$# zakk!djA|>KMQW|-5Y2-xT&CB0GIEjolx?)jKDV!5G*4u{K61Qx4hxfbN7|$tT`x#`D6$mT@-lH2o;LAKgqw7#qr%kZJWHs;%`m0Z%g zxy(Q1*{CwvrvJ0^?GKbc;-7eZI~@%BiZV&A60n~O>9K^AKOyB{5tD71Z?mpv&YO8v zF}D~lB$xYeH%K{>x;G!1ePOMPN$4WNiZ%b1JYWyioPz_G_So}p``nOb&A-ntcd(xB zV(r$=+AZH<%y)2x%(@dZTBqb#XZGiNq5FZo+=FYcdrsrrl$;y0&IDTeEXTMXl(>e! z?Q?^}W8O0x`TbnNw$97RI`V4{&a<`t9;BRSlks+YloFiL9DpdvCbX+T}R^79KV?PX>%N@=uIrnA7tNDAARK<5G z%nh;P^{N@K=5G#1=9wdE?L;bs>Ze-N#R&5@?d|o5w%ptO<0?F(-perE;;B)TAoaAB zy^lIAk1PVmaxZ!c^0XEH``9xo4%iP;ugvva^b5@OTyRPa=AsuN`VF+50U|?)z34ZH z{U}h}WhnC$W$ppRo#-uyyZxZJd&j~lkAdQDkuqPX%w?Um9p<-#;%=0(AEE4PLA;Ky zQCOw0OrcL_VVC-vVX4v&-*F%p%9XA1d0;e{m-Ec73=>(W+cDdp*l1j68?=34e_cJip+l ztW5eq6kUvEq&?rvY(cxD=-Mdv(c^7+{3*TAfZNR3)_PboN89UR%{=CMxvdlsH5kgE zZVt$i;<%*>uwZI%`naO60JQ$;cF;tphcRHSAT` zkBdENbE*68Lx;6--#}#gEAOkcNP9LAd*5B|z|eB|@yL3;teLm<5SyplYdVtro4XP7doV!RjmVLuIv z*H#bm2xT@qI7``-mX;LWUee6=f%5MzWyG247LT$Ho0XPMtE>!DlKz(Qv-64O7k(oB zh%^@lk(Yv%(i>V&w135tI3By-$syy#Nc40g{WY2RcOm(!O!*g54rEyc}BoBnw?hARnlxHo>raWec3>%cvDj^>;ePI~7U%82Uh%n*iIP~mOGZ9xhkVu^H?DQ)kgeU0j`8+5vnp=r z#5XR9-`cZIZCyIZnbCDrhYZne5Pwho(ASjCnS1k&N)=Xjw>H>^o`C3|))5cktNPpd z;m5A@-REhak;eCs?{RRCJ=vTgb{@juTh;f-9qoT%KX)kpi2ps5f8@y!?%Ff;H}jg! z)-v9{AnuX5Uw8(6=&u&vL|Vp?rW@IF9!pwXVHs;X?(pXY_Mg)zjNWj^Wb}y=p8gYcNW=C?hLSG)K1}h05})?l+V%hY-Ad_w^dr2 zmF6FC=0xSgM!tvn9`{R}b!LA2D*5ofGvn>P+qr^o-A^0UfZ%Bajq zeo_WCrhbL>eF}|quj2kJsRO6S|HjliE|a_G;pdL#k=Ub$(QIemA3+#q-J*W3?Li%* zU92q!H>X(j>Oy?W6d(Rv@^hXi+>LABnm*&Z-@4c8(=9^1S5BQdgDsq?)3wgT(yO{i$HNVA7p+xZjOby6`;7AZlU>IKyh~mG??oHY5U_wS(rNl zq)m>?0$G_FkfP{z7UrHK(cLj`0!1D^4I)RI_i8d`52%3_+yJ7JVBTXT{#8JOxgw|2 zXPJ7)67KELVD1P|{2gZTjT>ZPZVo8^iM%NM=g2#un?UjRw1v6HLHQ2i2MS*T&G>*> zn6Cj@gEi%QN!J``+S|Aa3v;J~l=rwY3v;Coiu(zmggef{+#HZXH}3@{OT<{JM6Q%) zJF+?l#NJDpNjklRA>$WCVAdCtre8N{?7X#>d3=q+DurbVeF}3F0#fhzexl}1a8}Go zaF$JXn)8GN=O^ytm>qT>pxU^6$K&<(^dWKarn2G7k}2^oQiRP0>9RE%jBxx8=5Z z%HB?Yna~mGxmoCl^xUiXIn!W1exa!_wj5*5qayq-hYObQbA1&3*C={V6fJHe-Tx$t zmU4--e=o{jE1g-naO%RA+xy{;z0~TpOX=Rt%OM8xGP`_6W$RDy#q8coD;J8Yb!jP| z5}dJMVd;W}QF;W;0nMH}f6k1#GlIlr_B^;Ojp~@b66NyX!Z~e4*_>S2O5C$6@0DxT zV)|2BK4)IrmMtSx8k~xP^c<2h^WI>2Y3ab`flc%vhN}!2$P45!o8$#@s7>+$Im9N> zdX612k}$A*-kj+JVa35<+reN|w9OgkK;ACk6h}B66m~I)g6C;#K6f?R(y{JM7&=0! zQCoUfQ)g5LXP3^II$$bedMVV9;aATppDS;{+0C=L*7G@O{+Ut{bNKPlmuef$x3B*v z<3Qv?pKQwgw(-Tz=WIWs{lmP;Kytp0Gp zT(yK7V=S}nzHU52_ zPE2gizzeO{9S>N{Wy-(zNw=ge=QeAsw)CLe8W(eR07n+}ikR&ZbnmvjRn&Rq6A#K7kZN@@H4p)<7z8=;?F@Qhfc-=0yI^tL4xBN?x zOZmG};WmY%!Fs+!>i6UX{61$ee|sn(`6hMLO+72m>K$-n?ys&7EX&B7H;wO=Iy?z_ zeyHB-@io2caLoGobm~ocsLs2byq5REP<^0U$_TS+%vM0>yFG!2LJfhHU?cQV>iKVd zS}>lxU5j7)$dCQJ@54=e_7%bX;6CUhl(W<^ca}$A5jyOBl6JBcyEibC_v+B$!0VKs zm_3Bq>-hafs3EYQa8l~)1991x2X_)q9J~hzXKU8r;4bCI<=E}Q-BxIKk=xjcKk*rf z`r%NccP;h28asD(@1VRlh8hD2v@=iQ44Cq64#e=>B$`&*T_R=&rRUq=GcFGzlGr7WH#oF@opn<^jqChaWpO`|+F z6OKl?JB5bd$G2;ppaxykjr&UztGF39qaGcMNJMh_c z$#efQwB6i~uWUc}qh8**=jL_bKI9bE_ETBoPh%ZBoptXF?xKzh>y=2|^f=`EuAIqS z-l@sYy806Cet6vN+Ev_3EBO%gfNzfC+pu319PI9h|9DkT?w}q&f4Aq$$4@@4c|P_) zuL|3{KVH>?@Alr7w?q2tQ1_}_FXz#`OMN%_rfcK9)3bsr+}fU*{OZu@_#Azb*FJgR z(X*`mTX#ywJ=KJs$rrBpr@nN>J@u(e(x`3t)K!~%ly62ia2Lm6zO^g&kM!)|JDDT* zSS<1ED0aoKb~v1Ghg{BeF7BlhH{yRiwD@bA#qy^%F~@A9iR1-i*V^{vHRIJ<)?U}L zrndHR&2^&TGOZ+oHe9pfbU>qhr^0iMNuM?81n5O3-DJ`!(07>hIg^fqe#WFvnRGmK z4c~iB%c=>yJEm`7B7KtddxOCQ)t{t*(pQ=NX9ay0^geJ1DExy#;qL=VKQ{;z{>#Cw z&{u%MUlX6Fr}6$j8;1WI2qXU+2&3Q|2&3>D2&3qd!>GBoHg{S(pDy*wucaAMzsj{# zV|qcJE_JM0OE9F4^>L;c^9%gC)Uy<4oUwv>t*3cT-ZaUUPkI?)XuefeCyAuyebriR^`B9DB{Z+w+EVH~q>ExeHy|F8vyJ zncv##exRj|vcFeCUv>+9+C=)co9W{waOeB@9Ut`y(D&U$A2@FAXV=|0mp;+l&+Lp& z*AKPMZ-2PdaiZt9wG!?L%#eP?+q-IF2ikXt_=mbx<%ZuoN4%#;ymyFrPmXv`;GOYX z(pW=Z(oNd7>LK{6p?^fS%Jo~voXXq5oyIT5)Zsh0U zyZZ*Zf|J26;7BkN%mh1u@6ql%gJ;3(K;gd@6#kK*@Q(n6|7uY9uK|T$`iKAdF#O*@ z82R5o7zN)z7=_^*_g4d=Y$ zG5>RCG5@n^exuOo7QM8$!zjT08syi*9cpu5uRa{8CX7d*6C1wp5+CIBoM0%qHn+Aq zWRCG(($ueah9PaZ1L;rZ*N$HjKQF)b{IrHI0vY(b%AXO`XZH5~lH$MP{ z|3{$kuLjNdfWrR>DEvPJtuXxGKp6SoKo|wzKp2I(&#f8%(`B+LsYsXlA>}A_N6Jy^ zkCda-At^_xM^cVbmp0L+ZvuOzeP1tkw>QS~UhVE++z1Z4`ayj|e6lf)_kWISXN(1} zx_XKJNL-Rx#{$rs;=IQ1vz`pR6YmMQ(sFfI-5GDj!0Yv-_#1Rb+Woq;;r_JYO^=Mx zOX}*qy>50HpxJ&Nrv3D;s|#o;8Ad^ZCosL?a9|<0y8g?+9`N5AP6rNwKdb*LumSul z^l5NI{kg!o`cDF93G;LMi}$ie1z#WNHagw8Sbr}mCs4hxt8r~zo%aKOr(pGjZpO3k z7VH1iaN1iwr>oHw_mgRNBf(6tx+2rqh`(oXch=uI_yzuDPR}&7I8Q(eoeAtEjE=Om zDy^eYz?`n2?r`AS*llL6Ivjd9b|ayCG}Hxt3hz(x^B?j1j_lom68z-@2u3~Tuk-VV zJ`ZT_j)vpb2lQ*nm&5gk0~r&Q-#)|Vx@jo$cIlHWBb{*1)Gy^*>4^ikhEA}qaKfCU?=O5%Pa&-`ug|DI8R%Gl(t9gBo%y9u9w#xIb!&n# z7@kw@StszP28Y(43UqYN`uXtsGu}>KPvC0!&XTsl^=G~N#$^}>Z(gET!e8l64}On4 zSVSI;B=7Dc?<(Us`+e~9|xMJ{}mIJux@-d@HS;ZXDa9+^(& z`#pj(N4DkDUSqTpks)P%sUeqcS`i9J`x%O#J)k$Qs0+v#IGQq^2K@x(Bl*67e9wp8 zvZBFkS1ZW#EznP{I2_oAy{!N2oaob2$69m7bg&ZHF9DRfV;U&)!*!s{Dc6HyzY-Mt zM?taQJ2_9^#k=r40}9VJP~B%_qm}(cW&f)M1^PXTr&96E zQalS3&pKtlUfHiy_Kzz2m+vpscPXA{6wfxrvs>{TQ1%Cv{cn~1yUPBRo<;fz#dAvW zG%B8xif1nEYzO^x>X^QPrv~`+n=el_(s{4EJl#kz&GPiomnRr$yf3&s&3I~nUvH0_ zIPz200_?TU6^eccl(m7aihfejZzy`7qK_#0sG{SDkLe#M?k`vL6^brYbdjQGD0-%% zS19@+MQ>H~lZt*r(fbsAMA1hT9Y^{mT;4qA+;x+t$7440@_1w07>B;Ii&Ou^>00+V z{nhW#=RCX2v96JM*-mcV-eru|dpfiXp+EhNbKP9zSua27GiCm^lXjO*|Fe@eCwZ-Cwp zrhxB)QkPGG!hZ%7e!Fd)h8F(gpzxmrh5y+*^Ys7uF#O*@82R5o7zN)z7=_X!R>UUBNsbj;wlVM05d;Z80J>@$ohSakm z-$^qL{vlUCaNv4<|L<i-&;&G;^D`)>GNZa5#fq3%@R zD&(<0;n$z?vxEQ0+-wwfD~WGo)`;M+x>Mf9l5WNy@Ut=JGGj#D$AN#s&FP68cQ+_S>yTx`wG~3H{9CUXk<)1!`S{4=58C1K~856 z3;r0~0KIK;g0Yf)h&4+c>vVnwKk1V``rv%YPnJ3kw4-mXO8mZV&wcBV3$`p9qhF3p zcSR`Vwa56=>;o(#j~n>aBX^Xu2XL65>_L4iW4r9R>Q!W$1n=wnn&mn}{1FRm`(a0e^8hd>jb5bp% zGxAO+L-Tbpw1SqhQmvL@w4)DR+Lo-8(kv@|!rgz8mx<{5|9P3Vu2kN$wUUoG&v^sy zuMN;j_R`NErJolWO5`Sym0a}m$%HYTFbbi!(cep7f0DeBv~Q!oKS5q?L#C2ElfFHP za=M$}GssT2&@QB(UI7GAjWszWf+`1u^&vWAGKV;M+}` zt|aag1C)iMDp%y--2wIyqT;c0Y@R+JGZ{B*yPW~QK0%qKgO%9r9Gf45uOJ3rVGO>a zFrUOr{G1)duZcK=)%H28Erw>x$h2v0^7j~11-C#2g{uq4uG588%@D;}3D{8^F zlepcJSeu(tlt!CP2u?_>%H2iWP83_=4hTDkSXx>H6wC%Qy_3M3<*-7{L7H*%7@t16J!MK=lycq2{^~RPiuFUxll~Ao;k}wRX1BlF zXrGIqC-IZIJF?-FSzmwk=h1o=bLU%Vua)#i8TDU!r_j%3i(A_1`}BpUCw2_?31>oo zrEPx|I2-z__kQ@kGW&!t11~oGHSivN#6IZCTeFOP#9?37)xmw!(~Ny(DaO8v1Y?%$ zgEH?}ncdm2_FfNLd$06a(pO4e#|PpK*(d$8)4A>}?N-|An8W8wjxy)5+ir3F`I5sC zZPiITO{AUnqMdG~jY|LaB<(bjb|`b1yTEOJpT3Rqkg_@sNI-B!O} zKZ3oO?F{qf$KWf7!B-fAuc!szR?2TQ?Qv^Xig7vZQQ{?Yt=|$>WZdM=)Og}1GP7;B zv#VdX<8Iq+^XKcfy>0hoc55Akom~rKxGf61eUfbUE_>aFLZ<_NfZkhw1YLk5fvt@FS1^u>ok zvzobRQP*_C+81x|n)~9R2I@eaIq%$(Bg+3Q{k8PbV@^n!SLND$c2B-z+#q#8 zm396x%&AV;4g*@G&%pL+QzWf+`1u^&v zWAGKV;M+pJ&Zsy$syZXya92DxYAgF$vc@5CM86|AojiJy{F=pYCi(}B(2@EX<>cvW z?BUO#-H1-d6Y>tqy2)X5l*A8Nb2&ynon<`hPF^*fJx|_L^7I_u#mou%#Vp>h`;=KTUw#a}f*5>-G5CtYe4EJ| z@zckeMwPQF_aC&*A1`GN*3hdeSN7jIN0IRb`+IWE?@M!E-nninZMmL3{lEOksms1! zi1l|{Cfbzb)9pC+497n&9G-1gljM&PulyK%1u^(olWmnBMJ@Pb&!esv@p3zjI`$1h z32yzv>$MZxN!u}Jm$!YcPOBYx*11j1)p<2{a-er$eKW+xy%3Rl2$IGsC;m~_WnHHS z>pHRqB>APG%TbwmevgN|;!M~VwN2Jtz31k=$R4MIy-p{4o-OYOIltU<;EXGQ^OSLX z!^FCuqltSk2#j>R+Ri>#uazT(vG@i-p;M~Qc=?{()PtBD>4 z@#UK%&39-tmHJ6r9{acZBEIeN_31r@ZpdlUdxrFi{)l{EyGf<@#e}>avC=z@v~K42 z*NF6fIZxXmX{{rj@-1phx1=NK%^YfVndKG#6)nY$;;G9o>%{rg_ z*>%sc@9kj!+llUvYwnk`+|-vi>PtL)354fC@5j0Be17}^`yQ9Qt?EYSrcUg;FG1%= zKr9bTRa*>uvp@jE~NCyv)v+9HT?eKA z(*gaT&s@jBZ%0;?Q za~M)rrOwE?e93F6kCLwrrAxMb`E9LqQG73>ixQ`0nDvADQ;WP3x(2-x`+aD{`?n(A zuZ(!_!#lF>r}^3!VRwx6&$$kblDVtsrQ`^rlTs##K8jBeU6dL@^iZk<(Lwq3$#MF4 zEpv5~gK?9w<@2RJ{Yn{Mcy~}%TOYkqpIP@e?=aR6{^H;cf6PU1Z4mN$J#vcZ!AY4O zU>;CjcOuYmT~%%>YbY`oIh#E=D6(QN<^c`)RkXWXuqppDW>1^s6GHKlnq+iKBA?lY#{kn|# z%wB(;4wFJZoTZ6LaAl#b2iEcY897%|-e8p9t5M338O(jcG1p z>(~OlM!9*?i`<~>YHfcMZn?HduTt+Kll_wVn0bR{gpeJ|Rlnzemv}iauoXE8L*)5aH;#8OCXJDgL*@zxsv@z0(N^M z>?dJY5@G*u*d2gMUXPXnR4PrbH`n&a?L|7&xvpl#0Vl3_IV zsLieKk*`bNVYhMm1lA|d1N@9 zOnzz63^amqZXUqZJ#IHvB66g>ZN1QbBVixw2w2M~==ltD$150pgqN2Mi`UXYcpy*OX zpT{rZe?^(UqUcgZf2iok6)pj@c zitelEVnrXrKjGi5%(pALSkZ4PdX>To@UaAsaqP<4+&5Po3w)H{J=mSG@H{Bx7FlM| zWffgUk!2(9>@u;-LdrqR?J|hW|M%h(jbnYRICw-h&GVRfV3&iP7fbL%^4HFjCB)ID zJ=k@Ru$Mft%SrhAVs}G?z2uo)Zeo9oyf`mE<-g?ND-rzLv3n)LUh=dw!v0O{K2-LP zD_jDui0^KQ|EqX6;|(?psLh?~_81NRq@X)LGuW8#`zpSnk#jCr1Rn}DdTTY8;k@6a zXH9nN%UEBzjXjdM_&7bp+}U6~u!AdpU8c*m?mTjNzvs`FJSX!F<_fl~?zvnm*{S3x zdk?_Wkw9K*kIeNXYfLM7zlr|w=|Z2`_dFXIql>(A4eP7BCo*2q7YwUE9oReEr;9F* z=;3tC9vpu?kzp|V0 zMg7O##q^zjrEe15sk4~h2>vrDdQQ7Z)5Pp6gA?y!e_?W_F>yhnG4cNH#>6|j8GG3S z$)s<&)t?#s1!XS!e?L^1mY)`UZd#uHsiM8qE3r=-)5Umkre6;zdLVcw`$uB;QZQft zA@s!hquzJB4>8Sxitm;C3-mFVf2wFh;nn>*8QZTY)V*UY|DPIIqz_c@0fj+uO`g~I z1$vkUWn$!cTg5jzo&n|mK!rb47*zPFLPMb)eyp%{Gwizmm9SsVmvp1E^}mqrO235N z@V^rFU^DE)|0`kdL7v(``0b@18LsIs-{jCIP{&iUuMY0I*{S~mJ>&NDQ<=*h>t2yH z1onV;(pR-}$v$R=xsRE&T=p^P$JobA%$GItjE@`CK4x?u7QK(TkFq`>dolEhvW9=q zd}r;QcXLgi^<;Z(|3>2Y0(jWhID zzruX)gQNAk*qcx3?=fCLKk`57KMK5uKNSt z0KJ--*@i7IKSm$(*xh{tyC&x86DMD0tfNiJKB8?uG1%4EHQBGPgHOz_25sj`ot=yyJF0T`PWI{dVzw8%XF>7rnOmE6cWk?7i0d=hS7QI& zFkiE7Ofz3W48CUFlV-l67JSbz|GbyIpvqf28SB{xTF=~X&#gZFG-L1v!u&B|N*uS5 zpWAqseJk5;+pT_mr7}AMzr~jygRdY4UttWsqA=ec;w660Vk}*m-O*UdzU(%}7<>QA zj)Ux-JwzO2FUPifl7050xD~TQprqqTWaHL+1u^&vWAL$;?N2o#{ggelS?tZur*7}d z$}k>cZ`NR6GnsYNOxo@eX|L>MN?W$uZ(sH@w<(!JbU~AlGyKRIy=Ygj(@q~kj(MGS zDtks_sN1q{w2L-&IeWi5F_W=f^lRp$OEC-miea>oZ_^eRAcO3o9X)&NX#FSXn27%B z^ZYg>l>H?fiL^Jd}DmcxI;YB)SVlDo+oRe^>Y& zL8*U{`U``7slgI>ByV>IEFFf%q69B0d_{VEQM?i9r|kD#PTOfjCaum$GYV)w2Ks-Alz%3Dw)E4om${8PVE5Zj z_A<9f9iaSWAKTu`%&%w~HG?vi{cPETeX`SNeI@hLoy=3$GB35xKObpv{<)fYw49q3 zeO@^)y@mb36`<6c%R40);?En@EAjLH%AWXZ?NSU^#q*;K#vKoNl*xG3oqTdNohM&5 zjFPogSxb)G%M^WQ*~^su0olv!KzV*PyZQXWX3nz6xdbu$k;1L)_5M=5|BFJ|KmSa< zcc8uq|L#D6K3=`gQ@H2WLj6bT{g(=#y|YOF7xn&`psOnP3CdsAHh;oBqYl>Hce#FS zt`kS^E!@Q3=Gi|w^fL#Y`l+{F`ia-vdgDLE=||Sb>-8%W^qT8N=r6qHJRtiNd9+X2 zZ<2N9^J*W_@)6>E)tlYAc*UiQ7%cPwTvY&3gfMeL0gvhP^HnbCaq%>3+~<#A^8cXQ-B zi1;<}m65JL#PY9(yJpRDi`vJN{niaW?F4tem&pFzG49{3cDg8M+1In;xL(uta_wLS z`@Ctq@8MpQdsaEEJ=5s@ybJC5o?_4UH2Xql*dO|w{ll|_Bm2Hz(6+vA&sX+9Blmp2 zB%ZPdTF?Gk19kE+d3A)nr=!HV@qcI!^g?MnZSH5slO}7Aw~4e}Xg_#+g`tR z;t9$_(zu1X*PKq-1bjDuf_Z?&k(Tlau$b6smhHH_< z?_tg25athJkDMEj`!^Oq5B>*+3t{H1&K~qgk1q zXCfi>k=BR%l$)c+{k&aBe;`uLt8^l0U6KVU|(!4ev9nqZgo zlDIWv^djFhrPoE;6(QAZl;{jg)-CSM?WpD zOP+4)r=`{w>$~yi8FZC1$b&uTpjrCN$EiQVf``Ys4SPQOTm0OQZsbt-4zdp^`ihpW z;!yOl(hNU$3&nNuH~anAX#8%(??&%wc$@uhH2tm~hi)VBHg)(e!tX-z+e!W}^Q0IB zpr5>6MqbZ+LaGATyA9Ni*sZlvv143{BgA>}J^9YY$V9HmUh@Qrqf`!e~)`zZ9i=-d4j z$42?7U*fh0{x}J{Cw+7;u$(+!dv+uO>>=;&+8T>GF0 zck6Y~qO)Jq`dB_32aqva*&kQ-au0hf`*-?U?zz@aqb{^^-@Bh>KffFI*vANeLxE*q z)`t7j%D%1e_ZM33CGc4B+h1hatBAGApF_%ix>UT>R@GM%_k+0~{vEJCxDmXP?@Vp5 zaLO8R5awlIFK`%0JO*S~_H$A!obnFwG~I)ydkd$m0mc0Tpt!#s6!#-6`#Hlbobo#P zAogoP@uvzDe;%-K${pD&2zWZ_QKqe>SJfPUSL9xGsYbrZ~6TnX3I8gYDLE$d| zh2IAXe-4on{!FkFm;wsF2NZsZwD30(N#XwlybL^MVeb1N-vbzT$im!rz;4iQT9{i4 z@_m4DFIkwo2js5LaXT%{-41du^tde+=B@!HUsqW;r3#e%n-3C|0pmc)pK6!{=Yv%7 z0TUEGN?~7R?pE|d3XOE;&Id_n?i`SG=1vDmXKootI&<#;NoQ^WDEUpL22kwRS~z7DDDhte3SS8*^=p`AKWC7IQ$E2`+&>SB`#ly; z*$9gJRUm%@rt?qS7l6X&vvA6}D=hc#fD+H=L8(vMLCLyfms@lbl{k+4{)GN09y|sL zU6O5mzo7t3fcAl6KME9k6zAk#&~YQkv_!Bk=m9f9;rD>T-$Z5#|6?$xfEz8$T?=-A zUSnZyHRy$|vM_fM*dBVJg}HOUcF+|T=9YoU(05pvE0K}(pCdBhl;fbJvj)U!+?zy3 z;`ux%@!SqdJU4>;ja$V(iRS|#{JD!j_}N1?`E%!h@aI;5{N+mj1b=QZDET`K><5aa zSue1Z_}pRPl-KF$-1uK>VeTDtZ!T!*r@`DD5UX*t9YOR2&Hg(+N1^nm{Kbc`7pzrS zqp(V0nL?k!9EE`C&gra;dp*wKyuCKg>Ad|8hcoMTr!y`GST zW8)7TkNv=*{VSOsc~AT{q51i4lAP1DzeLe5D|&*mXT8DXFH`gpMbB6CUPV_a`X`EB ztLUYQu2J+oir%T{sfw;u^bLyMujp?nI;80CiaxIB4vN+SR(k%X=oCd8ie@#>e8%%% z^1~jF|3ldiQuex{eTx2(q9-W&K1G))dbXnHD|(Wms}y~WqSq?AkD_Z7-C5B)70vfq z%>1rZ^ch9(SM(c-4k`L!p=oHx740K!9r)P~-L6U7^2-Z~w&kG@6~8V2q$t{!r|2sz z|N2-yKB{P29(zvl+wxmT+1v79m9n?xqXp3Xg`cu0dP@|&8qP@m(WEz$miI_{bQHZf zik=chYkU`SZu$KC!XG@0`e4P3pn3myTh`NOC~JJFoWbW&W>u8(!Ha2g=9S-F$u~9T zO`m=5+|mX21{cnrJEOE>YGvtE`_}NVEl%&Bv0$E7I?V!g;dtzblun(=HQm9v7l_b> z*i^FkV9Ua(d$Ih}X3U(oV1`7q^%pKnRhE~QSKQrJGG>!e-<>*p&eUmh z_$*7LA1yzD*+wSW(QM0mRcztpYjg4^j2T`!(!{|EhnMoXd&~zb9A0{jqAeU=dbOgj z4u|vI84G65yswl?)GZ(R&Wq`Tg~Lnv(2Gf1IJ}fEy_j_Pi!V^aOZl9OX=>php{>um zn6`Ye#k93>P^pE(O9zK7crm}}B7DP32Pu9F2PqnU+k%%%{@%+qLx&9GI(?p4_vDAa z9b#AZ@D=&d-w#2I(9rqSv>bjs$jsWR$OdhiHvg+dsFg>)HztowBg2Rs6P_4e?EUyD z5&nsnO~`*aARbi-*w2OT$G82-Q4Wgn2e~Nxc*AejOKIuXe-Ct!iWjn_`BdN^-MrRB z2C1Yg8w)OU-@Z@zSEXDnQvP|Ew5WKt@OB7%u$Xzu2*VqUJOlYjoccmiZOpg`-|f)M zC$vROikiS=eiQh~gWPHUq*%pV%IQ*{k9xhp-O&#A$DHhwwf+wHN9@Dh@X_K&~2O!=#JQnPRp;ddjzwfUxF6hqMvoo z31&lQvk&|y=nuPJ9`r!l`m1aG9^-v@MHguLG`I2ZyvtpyuifP`B#x(`C4N1}JB>f` zF88rE66SyK{xa{IzUwe-J=v-yn&~E;u*A(9@Y())YlI#mpbpL>toM z$QL*H;?j?IcItNCdEzzBR2S;?9jrV{As$D%`Sn!rB=@?lQ~pW*^+7j7%!l6W)9+E{ zy~#^4yIIk_$U~tAD|%T{sv&f$qI-~MLLX3c4)=Eoy-U$oaQC{bZ>Nzi(c$p0K02dQ znwgH-tc@P-9!FfQbR6mC)TgESPBQl~&kj;Y^w4r`T@T{wQv2SbPgC!TIR7R4#p#^+ z$Govyf&K>JZmsAWkoDR64sp8NBXFcczCI0eIs3dU%W1eVI}9y)B(m09pYAsKHY*-k z%a!%lqv?KqIc9QRt(-OdhGdt?_nRkY~I`r-USJwf6|oNvzR74W#au1>p&JH6Nw86596GPtuZCA)V(^o~-z z@y0P{*VR&2Q}ZIU1Z*i{mt-89h%X}D!Y3W-7~wV>3*=H?Xpf) zw3KZz?bO!6noL>voEe7bam@abI!JyT&PTtYo0OkZm$X)qR@t+#%g#Z%>e*Yi%db8? zUtgNlGZ5;`T?p*)Hzsh0K?lFSuO0XLW%c3P5rc!LZj>`1X+{SA939QwUZAa~BzO4R z@mWdyj#7WbekS!x^uYFU&y3{jO41-YY+J`A7!p6JABC7n-H`n@p=T)CtSjkO**ft@ z>WN(kdch;*&%OO-*@h^GI+w=T?k-PB|8~a1nY4h3Fv^(7)!>&-yw4oQE#LZ|!ya4HxW#UyC#Q;Ipa^79D^y=03S~ z7FzbRTlK-uMfSlivkyMsst@iQ)d!EEuPLUCN2@-#=>PFPnEWyO;6(DpL%t-?2gj>E zxbw$WA3TPB&B`;Y4=(yY*avt1*y@85iK~^4Bi+zdYS9PB(FZ$Y^uc4A&)}MUF!!X; zrW(5yNA=fs}13_Ybb6pOE`!L{CEcEnDBRn(D(CLbvs`_O69wOYhzaLz%1m0lkND%4&mGVX(M~@ z5}$)AKGR8`(9bA(Cg~IU7DX3!N;ibARP@EBFN1zq^2N*Ie?UXgydvr9kvqhh@teYvBI z-G0(7-(RU8Uz^*I?9er9Z#N+Ov^`SdIg6O2Pjz*9yUNu?mP)wCL&k%+@OaRJeEufG zmv5ce;oD=Z$Vi77Uzd{SJu1)(OdhNEKz}s(s?wz0 zn|G9;*Hx$V*W@mQW6!l7lk@QZV#ni;ulzOlYB=T&qQ4`owF%5+;*dw=y)*CbtZU44 zNBeSETzA3^_~HL|vCTM3?S8f?c7%#9rnRCEc$H zcI@6K*onIl|45iJH@O2|nVXc)k25ZV{ywzKC0<|XHe{}H{f*XKCGP$>GhAC2{@f9jK8v9xKGpNwW_(87Xywn=N-KYUhP}k+1;Us7dRfu? zz87!B4_e~axH!R#&v|Hx&x9o@W_+%Nmi)Pm@Fl*vHr=o5=m&G@{c;x}+%l|UMmov>{v6nmR|Mg0j8J`DL{F)ZK&G>X5 zW98SRC2@w?4zZVZcz=#&$lP7dvdY=hD|*D6@%bIJ#AkXRz9WXceQ)#f?&zaIUyi-Z z8Fq61Tk@|P>682{oNLXc21Co->fO<4X8iAfmi#dq>uvlJst7xe=>J@3^?4Gj;Z{Pmj-~a#je6snTnR({+%ri63T+YngXn(IF$F8X`)41rxbf;e{#(7(a zLqi-I?3~FMZ#x`*HBNfSZS!LeY1gsA1=t_`ycn0zo>e!_AS>fD$kxulSzKtFsJ*AL z=8uXd?NZ-?42CX5W7a$774CW)aZoz84IJa8cI7Wf2em8rm$`R+h;QVCjsY4R+x3{op}L$8Gx2hFdY+eLb20oAFZ)*wG{|b!tgr6?)iHP{U49tqD(T&s*UA3BdQy}>2X3V2CR8Q*)tyXtEQt26M_nBY zHf!HQJs&Ys2lZH-(d!KEAEbSR(7O^f95UsIa`otYN$w4s zj@?r6ZhrFY<8!lpgL{|*|{=XNM-={8M2H81GMo}NvZoHlb$>>=HqL^n8m z3O<=8zww-0knFt3_6kjeee+Up;D~dBD<|hELA7s>f_=nfJgAy10 z2}u5T05ShQY74L@a1{_yj`|%Y*6}xL8~zd9LN@0BY2U^Wwto+k@pv7T3?-4U4f(;u z|ESl16yKjDE@}ZH=ut&%Kbh^v11Y{CKnk}MoTPBJAP~YTWS_z-22yxM5*HylVmgv0 z)@$R-Bu2X#GZ+C-wY6xcqo^2rDPD}*ANMEh-uP3u*Sfvc&FMJU|GoaL{q6SOr0h81<@hr;t8)83o0S~=jm*f1TG#nUh`E)`8N9CY zJek25wQln?nUP_&?vep>y3TzL%t%h)nH*xS3Nh2Vn4X>nAA0lt5Hq>y-Gl4g<<)9+ zuGU+N6>DB~4q69Rs6DmI8|tc;S9_bwn``U6#&y^wSe+#sxqL3ya!uB0Ll>GgthtH> z?%>)h?m$;5T3%jhwv>v+OtnDj;ni_oXdR*8Jp5_*K~6Ks z!l|_osHd}a&D|VM27743{x_8tvJw@V-3U5c*L;n`LmMU@7t)1lq1}f~ize!FkK~3t(YoeYn5k(`Mg{zLtZR0>;CTJ(rE!>Z?}oYe z?wEt`fpanAaSo>g_u+PZEz`Gdz*#NIz3VXU%4~f>p8I;P=e~TJcRbANndfjmU##uh zVomc^%)ynTIixksLmRRFv#wCgmma}+Xf!`S>v5N&FQ++zS1{LW!@f7{NROC> zc|V#r+yXP{3qQs>8;uWchMC3(e;GK$OLGaUpj*)RuYdYrFO65{!<}^21e}NP7~V+_ zdA`{0--CD3Eeht?{7>SY^vagSnqTRNFD{YyNJUSRX>ts3U_H{dc{^xH$Dwt+s|#b~ zdL-uM4sFC*&^r6Bb9dV?4=(e(19yHNx_8p9BgiZ1XNQs3U*m3XF|I;5G_In%l7}s8 z8&7&s0__0P=fu=HA-)fW9gQ>T>}HYI2N5UEZ6x~V1N3qZt`g~%w3lK{(Y{ORa0BX` z{s%GV^*pZ~lKk9{^!_1AE5$xfv3__2{5<4fe&QQk;^#G~IKy$-*W>99^@s5d2=V5_ z8-G7u@h=K<6Vc5 zUzKKB!&N*E%)RRPOgui+t#H>-%q4yg>wzEPJL4!$e+<$h@__R!NmjmxbV*qm>{WSk zAWt9O?btg*Sa3#UYf^6o&xK7 zsMBWadOOxF2s;CG5cF*g>v|Vfm!LiQ7TT3!v@h3TO>Y{`=5>hsXGyj?)`%!@BDPn(4^C}_T3jaix@8~QyGPe2xE{C+R|P@8cUbyGifx+;B$ zm&R7B;Z9?#OH&5;AICejSx0b|-xGMJ_NrpO-LK}(9;&eUe-C?Vr&=**LFKQ?XGpqJ zv^%@uPO?eu&Xkk{)NQQ6q1`!rH|CMoNA3FPM^0{cl8#fmbNFY$ydFVbU%}mkVjUUb zP#Zz($eCz&XwE{de_w*Ul3Bn0P3Fz;L*quZx8Wu>4?&wt?ap?XMVp9r#}Q9$V$yMH zcMktdrtJvQb{O?|t7vyzIB%NQ)A(H3R>=P0^;1G*d5~yFKDzgs!1)$Zo})w7zhs;8 zA@Zo^oGa0$QeKa2l=~lkiS)_#YKZqzyi@))oL%3#(Yfn3v^O+gPU~c2`UTg?7I1s> zJlY-zw`hALPbZ6h>*(Fr2%av%dPxG>p6G$m{@3u0KgGKVZO;^rhxkl9R6J8WRyzc@EMd|e7*srGCDc=A6Sk64ds zeZjW*5XYm=pQgD$YICV={;-dGm%eXYi*hDeJUXC&*4n&BHd@Ft`naG!^plX~!#EGa z*xnEIsx>C!L+jnxhk~>mMSIxkniA;3)|7O1EnQqw+ToaQ+8!p};I*V)SWC*rJ{aozs4ea@P3mnq#l6HlX^Qvkdu(7eX zm`m<+os17VkVJg5VW#*##@(CL_N#fmERUR5yn*9ey;RO?sx_BicC(3jJhjHLWo4p> zU#)>OHOe{eaYzsK343tXpk9YPQ-OP05uegw2QPDoc{(+(`n;>ZFy8|Il;7WCUQ@ru zvk7Y$V$B8al;2kxW5k@g(wXV*Gm38>(oZ_`{7RlT1wHX=txKBK8bwZhqKL1Z^ZOXi z3lQ_rpr?7kA7c%N;#+u~jPIRBxrR0u^i)0$k3+@>dWz59O_TX$vsyz^Yc!el2_n9~ z!kzNBeW_fd`3}OP_%^SUYj6u;ru?00l>GWB=&3I)1pkSDYTi0=S)z#VEa)lzrpi8I z9`tI^Q+~%{4Tk#ic8>3frTs+y)V$}qm2w_itwBETN)`E4^V&1921xP!p5srzyfpFm zAj}ls(n>k6uXF=7FRkR~$)!mmJ|#bEU1`E>B|oz;Z%y%C#qsrB)>r0_$twFao?k|HYaSo4YDM z=Rr^9IR|TC6rYlR2jpMKE9dWd7uOdy|Euydw@I!EEBRULlJfNLlApEUKh>B2Df!t7 z`4PG)_(ApUaGXt;li0il>o_!qK8f-peRS3=bNNZ;`q7#EQ2V3C)E|}f4veWc%}EI8 zxlAbp7iezai)w~cY3)rA+UB8GrhO}dkJ>2Z)_97q%vQRHK{54wPuC?89ewa)1Ygl(8H|V^H_{rzLx$g9BOh#h8 zMn82b!XtZn60it_@14Wk={sV2GT@GfrX>x}Z+Ig0ymSe3--h{7%+Ze8hHz-z2|7Mu zK7FrD>rPuF=5GMfde1x{t=p7JTvP-k|3iR?YE%M{)}2mbL50?V%7C=)Qw$`!WHz6K z5=rYrM}cI&8%T7U5Gmn7QtFt~yc7|MQDZ;KAUmD{}Y)Ia*B0$jZ&l&dD1$Zd_(Y#@Ng(*kt1k_L-UEh%hTF zdyHIo3SW1sZICNwfrYu?vXkk$6Ye`hgMl9&S~omsC=g*C_?J0EzT*Ba@hCIe2x=$j zECBIzb=@iT5=l?tQIK>#gRpXHEz(8)?`+*EgTvX!o^~Le5bMB$HpTFCw(iu*;knsY z%}}}Sw11d52f+{)8ED$DdQy$9wa@$4bV5XYYXaw=;YDn`5-; zZ{ZuPEub9>+9&Y-__Rd-*s?_Ly#pUF$p!5Z(7rLJxBu;E+w{uRvEEALZ4hY3qWt>8 zopjmRj6Y`F!}vDiDaO9gyNG@)qtbYg&wd7|OXfWQ4x4NC3~jg1;|tZr~Wu#^`9TFX`ncndk!#10QUx02H z>SkuY1eAY*m)eou_UMu?CP(?lAfKBN9)0tejWW6k<=R--8|B&COYK4_=&~^fNp-6a z6!ifxn=#lRBsQ0?uo^T z{v^mn686@g1z84i)PgW+zwQwi_D$lQ#)}zo3H~y+uf(^L%6@BHV!-}L9LAW6zFTs@{#V^n z0`{%lQiVP3>D`B)hB;%crDgp8o7xo7&xm#T@!weZhMI+NG|x zDKpXM-#(sry8iWTEYHPI3A(LHht`KTaJlWT6-2AO`ayUqJ7Wx)0i-vKVjHH0ld` zdD9?Y2{;d!`j&gp58aM_D5xhzmy9WMdR0F(Q1n?71TE<(*F*P`IueCx_s&FF(A`j3 z2+N?KKsS*(Pjt!HvR>XTXydZc#x)JRO6VuEQ5G|2_VLde_}c*8ILO!IINNxvpc^mf zo&eorpj!d`Wh{8K2kl@k^qF_i7UrP+qjQb-Antq7Zf=?1*Z*VaE{g6-=rSE>3rD!q zK{vs>6LiDVMtTRJ4W+g*3GLp^kVn#Qx)-GTHx_jF-j()9{07^SU%1v3?To!J<4u<> zcBE@i(a7{Pt+56BLh`;7=TAxhT?w_J^U>ywN4uMPQ@TGN?c;p3iSyAmK8v>Y_h>U0 zrT@MpA9T-vZX(*<_isbH>K^EQ25s^)pnH(o+|_CRpQk?^pc`J6>iy^7f&ORE#uLp% z(6@o^0_dJgf2L$2!s|eIBg)deuAu|bF1x%P>EpZ|pnVl>^ZSF-{U4-18=xCmHqbkK zgbO?%y0L<8;-GYYBj%|fuNSWW*xnO;_vnhKWbaKy1*5Wl?Yd&tdV9~Tfn!EzrD*Y& zL??Q$hn+H0nO_o}>eb5u$pDqTl7k$SSr+upF*+H@BN?dZ?QcZ+Q=2jdWxR^o73iXR z8Mq2EKx;jDIvGg8{B9r2cdvpBP#>2p_}<4mAKxM6p#Mq+f0B{^2Ox)AZcO&y2N_Uw z`H+F1u1o>niEe_R`#Ji|`<5m9Q&EoN5#9p`Zvx8knbj%&tK4bc2T+dG7cNF$IDciT ze<8|I(NQ`6{kAmlo#?I`?~rcLk;{LVoCew5>Igy*tFmryxQ9FZ<$*_Afd>xzPGe8=T? zVZ5Md2Bcl>t%3imp{vB=4DylgSnqb#!6Jr|c6DGT5pYU;NzUV*h zfo^>J zpgl;(+VHg&eL}LnPe|)ppOA|_;cL_dvDWEGC|QWQL4Cp^i#|cDNrTSAeZoSEK0(nH z1^R?M(Iz z9L1gl@N+TBk@|%B7JY)EqjIEneX&KKAijk|cvOzEPmtxfIHXTdbX1PiClp!q2}LMx zmo1@$%8~j6l4X6L&<4Fv(NLd2zFDyg9a2>*1hVX7t z{Q}x%NhkY+Yq?KQbgRf~3P3bWk$FOZIj{pQ`BeeS)NueZsZeClFl&;!Qw3 z?TfOSf$$13t{y%F_rIebpf-3gXlRV6`hj1={4&fh0;&C1{Xu9y;6xiu?tfT~Z_x1j z9)45s`xxVgE%-IzHxs`s{HU+^3HqhmU48w}7ux-|4@vMJT>$?WOANu7g0PBl8{>(__ijEg-uYZ2E9esxbeTWm|h^XZkR!3t$zil)16)qd* zHSlZeskPJkH1#DkS6Yblk~@93U#KOOOsdel4)hz73mx98(3ccq%s_K7*qbcwZkP<6 zeKP#q4%#UdcJISD>uFM9l(z`9w?igq-W2-_y_J|Vt-@GD(N3*!cwb75@=pQn_XX_; zL96Ch??C?`)^HK-j0&fh=FhGvjPb^a@#{#@@6i0&!*$mKj zLtpZ8>SckkuGou-K57PN6YNPPvntR>mBjd`7WNW)JKe29;}*JO?qiHwXpEz1ODk}X z8^*7*K${BvK4IW^LEDTuoCb_t`aoBjg>ah??(B*Lj7fU=ug6`2ebMGOK?Z5wbS&wv zXje#gRkU*|dWmt#Y|zqpp$T&{M63Ft*%f`ff1Lwe6#aZD!rvmow|U1xrnW%er8(9) zxC`bW#?xOAAJA!T4hi#w1$?jvKuNW0E!Ke;$XOGS9BCdDo!- z(es~pPdr!rzYjd!g8L)&yq{0J$G)q#!FL)%(!IccgB-kF(bvHHMd1CsmC643I^Hh= z@8^K`e*^F5BOmj@pT*$)Gbp2Zg=rWIjud0Tffx%WqpzVcqoOMU@Bfam!D2yonV{Pb zy1#*LF~%C=TX2MTE%=ubsT?l(f_-$#CEY%&sKlL>B*j+<|@D(EDd% z&3jWW+A%EkC$@DwIJ0e-c8hA;T-{q{_OHgh+x@kfBS8DfV0^cMcF=`3u#IE~-)E0N zojZniItM}YdBE?pxdwd%(fHWBfz7Y7c?X+g(N~cF{cKiuptrHPmfc+&BNL3 zV{;A6uj1X6Ho{x9#O3dnpB~@+JC0d*^>Dv2s-`Y4K340!Yp6fvQJdC#nXSjPWx!E` z{3*vB-L5OCt;_SlXJ%Tep$d)n1<0c1Lc>ndsR1GjZwQ>yDD+j-J>6nCBLm__&G5)>|>!b+l~#s2K~%r^f!}mS6tz8 z^gn_9yx;Ui-}FV_^hMwFMKpi;O<(l?pT0=-L(~U-=IFL_I{Kl}=#xITjgI>-TaUQU zY&}MP%cL(d>xUltALxf@BZ<=%-wl6Gy5EcL`}%jALpHC_))xBl)6o$czj9#=qYrWa z7mU!BbWeud)nnM(L(XikFwEV}o1t zaOk;<+|BOWqK!m&HdwePUn2GzjiUQ`Qn6o??&kq6+MR(kVPEQRB<4Q=#8$`R`y}SS zg>dmbe(`G(^B)J2|J@Q7Z35zNR2AD}FS`hL2sSuj|7vlD#QX#x_OTXsmzZCK{2

    3e2Z`;t(l}O*@}v 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

    H3-r%=-k!E@a$n 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

    J@LvVE_e~v{=Wjg5 z5*02$z|lkAkx*=C_*b}`n)364qu}=!m6t)QQdL=H-*lXx8_AX@Z_DMm`%tW~3|&*Q ze06cja>IlFbX^FJ(!qV|B7&(nGJh3Mt1HW}IR$X$gTAOf`=*Ya?; zr>bT-?`u$@AepKvv2mcR(u0HI_tjLb#@YYCHEN0bPS-2XhO#O{`p_BIzYCcM#yV|i zCRd|L?}ip-SuAwKd_2UM+Eh{_8qn2HRasWdnMz+-^0QTLb@`dpp(QhsgBF?<3nPW~ zdnrOQkx{5Dxlf-T4OIeFBI%{DPiCW3`cLD^6>1fFHOn62%N6;n25(j~puZ~)Iuc5# zummYB6%VMQq`xbUIueSbPRQ#koYh&?S-Q4~fIB5HUz{mhylb5=o;qDEONW{-t|{Z) z8+CP6XkUy;<52U(YuKPV3;{S_%sx&y(}3?U&Exa~qi{7}jOJ7~16@yM|Bi`|%!Xo<+3{;pi>0UBH-#CWt@BV>IP>@ESztxQ<2={OT<2C-pxa(%IgQ-_5;G9~bNovaj=rQHdSO3w%Exgv<1qn{@#XlM=`tLU z>BoKAOlP!<8O#rk-zdgt29CzOX$nkSF;5fiI2@fMJ=)MDx?%M%59^VB>_v-=azCc2@<-{o$ zSo{yzC+@Y?+ZwRb`%UX{>j`TX3Z8j9d=qiVs=jX=pK*Mr>S-Ag) z{+^_(dcl3pluy;Oi{L@LQ1ZJZ>8c&df<4GS!?^)7eoFvOy>ezc-_7`%0Dj*F(M*3O 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

    w9<-f zhDyd>578|*<;Ly*;`*d{e;wL(-m$CN%{$sBdEUAC?KxjL1$}=9^!};nv^$~$PvdOs zxX`*2DW5Kz+$YN(&Sjk&z3ADOaAw5wPRF)_K3d^lKo_`o6nD{nlXsw}E&kZ5-kfPY zalv-i*Uz7PUUMCDL9YtgJCCjE#XZ5d=F}KxIrpr{PG;{~@>0)Do|)Rj=r-zUB56VY zwYDAUO`o+EJ?vWaYHOd@948ts(+bn5$5ks%`!wo!2Y9YE=rabL0KM3t8x1-I`c8vB zXV7ubFB|kJgN}!;;%?d0j4Iy;jr_se>u1b`U+6^tKxI?RNnu&VR*lVFmk_zF!H{IFvfffVdP(O7**HRX3uEr z(WQKOwbX!=uM(|8U}j#9E@f=FmJpCKcBMTfuprN?OF2ui#|2hUuJsj%eD=1Q?#!_1 zQtsUL_yE_!>d!yyJWwiCU-qZM-2V4Acm$$cv(x2JvwiGQ$rMRw@DOW1qI zu=n<1?@3|r3B1#X%QsfhmUNf8t=frh#=F@@bT!`1_Mt=1UA}aKHrt2J#=F@z9Pja} zuM2dFmb^yVi0-84L-+Labpxk@UBQuHI+zZ220x4&z=akF*g|zIrD=sC%{U0V!irzDl%i zfxdS?q%YUH2Be%x8N1Dq9vFG|!}Kwd(^qVvwf8A)@rk* zKNT71)gEsDFaKkL9<2Y$XY)Gu_rk6hcGr7s{<`|FlI_Whb&;>{LXMu<@OAQ&J%8={ z^;b9QZ!~=4E5UpTGM1zF1Nu7+-y{pY7<%U?F;)AU2_6L({vul?JjXpk^Z|dJSEg*HcJ>Q?iuPwhe{F3>#_|RJ`H_5-G9>vZ|>@QO11G>*JCGzt4El_g=iq=Sa=g9d%zOrwzP8 zZxcUSx1~O)OC26S9bW(VIK8m0KDp1$jsR%XpVic#zIAmzEhR0Gm*DcvY&hs!1g@$7 z+P4GzkA~B}55a%0|Hiit{2TOXa9#a5-?{qFd}j#rE82^XdW`bFHP9L8eAh$zM{V4` z<%_xnuB)p{{+!6F|Nz2JLvlkb}ukq9S%JlyOGen8tQyMf%hl) z`3!#F*<-t}5PvzOC{U02Tl~DiuY8)bQ^0o1!}@ik%fb4CzO+eQ167ZW)gNu+4D`jn z!rX3t@nHo6@C{I zSL=hxB`IA3U3zTAeVuPdebDEe;tXW<*ycNpuHin~#**Oa8+5@LE zHv3AjyN|JGeS*k!^}c}3QTZ2}^H~4anhO7LL%Q#M`|O|oFPme(jHQybE^D@tuWxps z?C?F}R{WgJICu`@U1=j_+`oN%ZFa}{6UlvvzmxBY12+Xvpj$X$jMLvA^N60px60T) zt^TC1Q~k;0Tj1%!FNOR#iP@Z65(0zaIn@qbfwzNyX#FW)C;ObA4zK?*xpT71Hv+yh zeB0pqGs$}d#R8 z>;?V8iaMY4fn&+z8PFTZA4&Ixq_rE(b+SkAFy0(9 zb_C0j{SrVKJEnp%KCA>~O!*Wj_A5cLe-ae?ol|r4ZM+N5%b@UV28CxkC_MW?u|EKc z{qI1r{{R&GS7&(j6TA!0DNuM0gTiwX6rK(@n)V$*u}=WSJ{1)E*Y3{M2P>YTiswqj zGeq&+tn8;K`?1P?vaCl${;>RAo#bcb>Md|NU-`puVj2z2DV z{PK>01j8&xAA5O1AeHxpm!}4{WP0^>xQQb@MHgV{J69{yk12YSqF+?>+ltO^vT{-{{hlU%IqJ(ouE}8K7?Ke{XUoiegH~YJ_QQ@m!R-lb>lR&@E-?-|0F2< zuiTZR|MSD}ehXpbehXpbeG6fX`4+;+zvM7pp}ml@wd>IL^p1CQ3`qGJen+Q(l&@Wf z9??78(IFsZY}g%X0V!j<4lU7B?nntpIU90EYT&>hv-SP^Z_wX=XSDwQ+Yjni|9px$ zhF5+%PQSnYu&>eRPY)+AqCd6j$#uvhe-fJC2PYoVJJByoUAau^5cTl44JUm?*i|DV zNuT{uk8Ay}Hk?d$)t&S`m*n<6mz&{V-EiD@Yf;z0Pa2LVcZ2tFc%@GsUw7R1(G@)c z2hKih^zGNz|IJrM|1Nd=9{7IK(BvCkcglA)^4Ndk*MH{r@IS-YY!r4YiSOZzEdQ{& zQ^|)5y9fS=pNHL-1+wan`JTni>B(IKhx>I49G;sV_+Jetd>=Dz+=+bh(Oo?QhYQmK zqT~7_a|_rdue+s7;4ppam-OvFX6&{O8RT@2Vg4V1>!3GJO$e-HF5-!$wo08}fS3b!3GWk}%^VQ%P-zNAsVfNV6_`qR!4ioQ_!Q;Nu!Q;uD z;rR+V^e{Y!F3Zz@(G;+ZoWrjzE5LJs-<;7-sv3BJY54?UZkv4tEC0n(grVW zO;$>2mX$u^oIpv-MArU)`lwb}B=0@6!lUf-9L@WinOfma+WAjt=S7ARxk+Rt2km?k zVaz0qG0>Z7@1?CjN!rM_Z>GIJL0WA_rjj(1w!IDcbPvCmk)5VcFQlDb1WLQS-It?3 z$-A`4mfiNrUVWi5D+Eox+!%a$G5E&B;LDHT+fJP>C+?Gdn8F{z-`y+1rTQ zi2^hHA;d%Sa5Hfm%e%;#mfhy@Uj0gCb`pM*FE<8XUJSl5G5GQ$_%=)2+OZBHFEw!U zIFCNI9eGMwlziR89P7(_3iJ;dC;c&aB6$RL%&LF4Qa=|%-_B3U?#PByMtS|!r(^XD z#?DiyujRByY4u+xPotgdA#SOsAJZ0|p4=(aCVUzE8+H2|-3D5+V{(b zzxh6*jo1TSeoIDR4{_L&F~YxRW@=zhaY|rMX+mI*%!M-USlOdXz?^?QXwJXVW=UHq zX&vv256E2Ur*?bg8S1Un)o}-#3O`}YW7XY)`liBzVRh9`Jx!#Z_Mx6`qK-=Y_9FE( zk$Na&nY+QwUXQ++{E)ml4oW%S#2Kb>Z8O<##+B4tnj$W`Ks~6O=M=vZfiHMZpGcQ+w9HNEqlxE#U3r! zA#Ck7CWhPmklPoD*Ui*L@ncV3O5h6Wqt#bP-?8bZWA(Mn#ml_=Nq$c;S8t6IcR(+v z4gY}o?hk{feSd`BS$~N20Ec{===-mrACG^8XC&wY1UWPDq|tWLs@^Z|+5IkTRh1z885f>vJ}e|H}zuQ#X&k zf^U(0d{Mp$+}Jrse~b6!yjyk~Nz;wW>@m>f%Z6Gd(ioq}7knAt(En8kZ_k1}iK%Z8DE>L2 z@GQHkHr^QV%8kL77lRL-Y>V{BkKmIzkGej@%V`hPF>eq|aO!`#K|4{+w;i!}eb?jY zyvCMe?&Z`RUDj}B2kQ>ZyG9(G7ZJV=LB6rVj(?PO(d+a=uOm7jNiU7{9OdavJ6xny zDQhrl8_jclWqrrF`M+eI)5d(Koq5m5Genw}x%PkQNMMg;9QU7?=XW%6MuwczeU`Ld zpTznO<~f_!d30+`)hf<<`8~?-YxbcA)n3#u@6l>{InveyS!-cu-n4Fw&p}?3wHU;g`%1~1&V1R~qMUST?94p;64n4|ocq4&kLG%y z0x*0%(B2Qj)&p@SbpiCM8?5z0X~*^+&wP!uh7mkYT z%UKT;&u_iMv0v5$$=NPaRwPg5j`c?J`Yb5*=L_`CpQyYFQYKw$9nkJ2!)yAp4yc#S z^={Rh+|9!|?$){=@%IJH!q);t`xB(EkusCWPu2m+{9VL4AiLv4?+#jFI{A>i%!W=p z?bzyRT45^d398>S)20**Pn#p}My5?Eba>hv*{2nL#W$Mi^P^Egl|CBjQ{{Ma)OzA9 z=^^oMR(I0Jad@!mH`6})AblO(wi>&s0eR7VTqs;n%CD^T} z{cg#Z8-p(|2H%(%eEAW4GQYKcO_pBec*;o67jh%hqssBeQC6Sx!pg?~cxFTNVDe_4Mnxgq$C?`ZIw=JcJe>l^argCQUS0S{6Tdpf z5`BYT`1%FENIpzB@AT~uxB~h0MaSAvSHSyH56T)cYTanQU&%p1%9lp@x{UG7PH&E0 zh5RkFrf_`y-+j~T{_eXxW1xRMV`I1CFIM*3U1@tj??<(b=rqFHqt7wm+ ztvFRFy0v(O$D#HHEpQrHYthQnzIBWrMQ62+xQV{vH2w(gARNIog~OD&rT-{Y^e|<= zL(zqb{?OuAxK81RgrjGt1x~ZJ;(sdq5u?)rXQ$-o;o(29k~v|@_sHC?{+~!%sPvGv zQ)h|$X%&B=U*j8hsP7cIEAMGxw5*947RLVoc8kL7Z^y1M%zh+x!@}%e!)`~I{q5Kl zhS~pb>^=;$e}MNz{FYLWB+n#HKM{L=pE1wb!1&)XPoYhaJjfvp{x?|PsW#g?s%s$K zuIcTj+H_kRyT0+4x}YxzrUyUw`FsA# zS3!F)61}4vbP~TRX-lAkU>tGMNQ*hl>lTA;(0>N6ZT7P#epb>s5qvIS1ZjWO0Wq+&LRHg&z@{ZSnk+XCzTV|5LkbS>*9d({6ec{ceG zU;l}rLrubb!Hh0}-=MFnxv`(mnc6kbzpS6HjC>XR6>Ze=v~Gbi?1WBK=C9>@_2vB% z0{xYFe`PLu+h6v`)yFCGqvZ9EuZ}nT+}Sx#uTgw$kiGJ9y7+6b7k{^-I~a%E;*=f+ z?>owTV{*Q}TD_NnUT0$9??Y>||8vFPeeX~&b`wt1J^g%3aBHBVdn$UgqDL#bNYPFB zCH%XU`EErQDf%ypeqP}cut!|aK+`p~*?(El`zgAg zq6-v#1pkD;TA5cXxHkrDV&KS?W*l51o94KTG_dl)N{c1R+#SYWjot1rdr8xxF#EmO{YBY7uW$*t zBEDxp{9nbp5pS>|vo?FW(-mm&w(&c2)BT5YJ>SGP9A@v!75>M9hm&hHN5KA|L(iD% z)E`CPaw~Htaq)3_kg;?C{a|}Xd}X?WbB&P8`|oNhd{xF9j1??d-F3NExK+tf)|`N` zW&(Ms9WvL8=$uyaeiQBEOJh7n+w+QVoG$Xtwdh;7Po}@3Ef`jR+P8DKN0+rZvM#4n zkKz8U=$CiV7FcsAU76SV5Ay6+v|Dc@`}kPXa~*Com>V06Zgy~wEdR&Q>F8u-e&x)< z?t$OT%h4aAtUr|7-T&K#1^QHUx=WaYDrIfrQ?w(U@v{uG1-!rf*Btb`=X?v`mwAfr z$TIsdANfevK!^IjC-S^o zlj}c8{-Ea&!_2Svc0ZV>kHh?PMF$j)=-)X|eZ?3(d7SD0mVx>DK=tlZ=m($3Ne=v+ zb(sP3B#@KbBEHe_^eO)bD*S;$zrxQI1{7N1#|m3F!mj%t3Hvv>^4+ZA`XBi2a<7El z@IMmvU?c2<|07}VK%QDh`0b<}8LsKSxyhzaqKv2X7~$V`vt9o=>yF#ePNgriRqhrY z0&~J!X{*{gWL`7PnAdExOy)Id$C%el%oUw^+OY;TuNmEjMbB&QA+Pt#+zf4^==2X5 z@95%rH+1yqmn~iYjl}Ua=HwUM?$NDrKpOK#YpM6&q5j`bd-ybMn5?P%h_#fz^wwto zss3#8bM;^PhST2sA9&XB`%mcmzL#UPi61d%{62g2UZHK=%UskSnS&Y!@8Vn217Ff! z{R;D)kBrstX0ASEfGhAC>m~nB{ZZdX_*2@@l>8BHzJ~un=Bge7AA;{mqa7OMZ%cf4 zEM%R{>>dG2UVe@?=DBI{dN+ zV#n-U{X=MxjTd@S{XgMdWak}I^Ymf(lX{~oAa?(TxxqhUjD9jPd**m9t^!qT|iQOxp`1kTH&1-oqyO)XU%h;D= z|J@K@^IDl^zPuQG&1+Gb`SK(9US|AxA9F+Hw{#9X#k|l{jQw`p;?Ymj2d^W{9}%X+ zaWm<;nRl7Tvg|hB;?-9wvoGN{`Eq0M<;CC|6N4{5#J7WZiJx=mOIP;j6j;eT?PmHI zYd*}1gUqEpMjT{r$Fh5odG}9nD`p>p@*OWC8@J@ki@`T01|M_V-VT9qKV?pB4s*2& zDBF87(gKe$R~ulSGYLIvI(2u6)K}&w-`np`3`k)Au`Ag>d`B=jMab4 zniE;ix{Ke_$P?z;;V)Su{iSa$aczUY_u_sqduewt?`@^elcb@P)4{XT123|cAz!8G z0n+aZugx#zFMK`2U{42sAv}_{+kNJmhQs*@0r6uB=EA=Xoyci)HTObGT^j<5J$sdt zWj)L`-x&R7Wj7Q&fw>t@^EwBaHBAh&8?=$`W+W(lum!v zlXP-4HjystMu~1!bjjg!o3j2{<~C)1K;|~vlb>IdHScG5fxRuVPeIInsBjZ=zrRrL ze^4m%>0hY#_LLXl-|oxPC#v`P3U}NxM*pFD|AoR=?#kEypx(a_bW~(-Apb?T`D4x@ zwV`+4=J=7JCyt(5xQV&WGk>z_UmmdQr`~nwC*E}Gho6np4?PvH*RM>_tFF(|UwhNO zU*;)tsGl()CKht`qV_|{&f{d_@m zDQ@0H-zmC`kU2V-kU4sfkU8W0kU8V-kU3-Eka?dlbH-dDbM%-YesqeKdHu?qi1}O5 zv#&?DBJ-$Kq+2cLxyoFG_3dI;CG~_lA^K1^*86%MO{w)F8b z4`l0BF}eQ#ws$VzRaMubUi*<`hi9H7K%Nlgp}__`wmt$xh*x_Lwph{P;r?T;xx&s) zka$i#-?`s)ec2gn%sJM4j5*erk1^Jq*zAOlVz=sUjs9^tTNYaEUf3W@u!T*=CN>G% z*hFlQ#e73L0h?J7wzERMA$_A>&P*V!@?Tu*daJwicCZ(%kFj;{((M## z3%|5oIH5;&VPq-c!=b-ewvIgqs_`#~#@4ulFb{Bud@C8!hfFa%TZSh3g7$Yelkp{S z-&Yzu(|WS(3ErV{{BkBT{;Z}yIfXw??sp1<_Im1oH>U-~pR|B6!ZzZ?7jzus2=OO- zk>4$hFT`io{(j~z)*h~7Tz(tt7jF{&J>nVjmdXAc4d7$$DpG5@$0{StI@WcgcT5fL zX>Bjd;(M;UQ?3u}Z*A}AW}V@mS&!Xx@>Bd*uM6xY&kvXnyhvX)k@TNTxgl`V$q&nB zo&2!hXlTpv6HdB4$(+X=VEoDn!QCg?$?y8W!#pGFE-4>R3BJs_Me?Fpb3bdXw?X$) z#yRsyqnxq0tY5qc?L5|A_CohlY*w>bPq~HllpjJn|Kyj9OHP(e;NDuN_6E+*OE9mS zTO7QZ=j2V-<#$Q$o!~r;b2qdNRL4C zd*V*;ea~wrJ}VnWp2`P^^_qLqlFfpd6NC1*W&>L%2W5XEH_ynoU?1?Dd{gvf>$x)N z+h4^-)uYYv6Zb>d`Mtk5MVnpZN{5`UsojQc)0ajst+c-^E3#;uWNQ6Ki= zK?dB+*M7o%5jr(|j~VenlD%j)P}U(vvp1S=y?Qw!x3;^YpY=OjAAR`og^J<$E~j(9 zMEqR{pNBl!vS{yJQ({DKam8MgMcJz);=U=tN$__vvbTb^p23)=2v|heM&wg`p759I z_fx`d!;aVhEXRg7lXk*SO4X|f-?fN=g+beQE%snZ@Mom+Jbugx@L&)AX_l|^8RRD~ z&{pa;?YZsiqD14#txf-{m~;V$Js{&G;JWxkMpziIEP^mI<-zI`n|W`;xy4?lErB?I>+Jg|ptJ>>K2)`0@CHq1%fVw~sGEzU0|V(s7da5OnlVU=8*w$ye5Tig|CU zp&3dT{`G1fA^SaaQYL$kAp1MR^=_B?&M<>=d^q%%_15{&^fX+uZ@hg)N4omh_kBR* z-eHm1{||-XY9BZ|QlRWD7karv5$+?xaK=8JF4=!C^uiyf(-qwbv-Xw?4Z?4y)77UF z)^#6PXIT7y_43=r&9WF7UVKA`8?Bk_z1QCeXTRYdW~Fczz*w_O{5c)pdWraB!>#9U z1t1Gup3fU$#WxLRpZjj|-&JhI*IwfJJ`=6@RzP><`&h@zKJ(qhpP6JmuM5_NKWBA( ztq5Lh7x6XZ`CRso{}?zDxDR-hoO@-#`Og8_x;eWUI22d`q#W6KR(ySq1?RU@PI-P0 zP@dmy!TGy@^8BMfdAkn;mdk6O@s1jt^XrH3u(Jpg3i=+b=_ z^zHx(U$0J$^p57)P_4KX)Qcv$3prl_6l=NdX-ULFD^(!nmKLN-qn*Ap4Atc#5fD*smg7dcl zRr!HJR|ypP@>%iq<1IMAb;8Q{FCQPfkIbe!TAy?&mX$ z3wmpTy}>Ij=ye07{!%F5{Ie8F>bVn0Q0cEIjFj^LP|EonP|CR-$Y1Gw{F8Dv1EKfc z354Fe8VJ321rT~~J&-@IoKFe8w-hLHA?G=b0J=3^OQe*i(t`63qv_nFf6#(n(a$b$ zwr3US%>WWr%DW>_o#%lHjm+SmK=~)Im3V;%HQb?Lvxb!#`ZUbY5TKrOIuAx1j&L~V z9E@-}=iKUWrp$3VQ!?DH)?RJB+IzWO%?X#1`&1 zC2r>aT*4;q&n28sa5z6sI0ya531_%(P57Am)&xG4a2+Z8RoT%px9h<4gVPUBce}RF z+%a?KOz;^8XB?ivseR>V%g>b)K6l65opYVe^K+Z$Ii2luKc4GwzB9L-kay;`a({I0 zJKP_g`zH5?=N{$$@Z2NZ@0)v=`+ai{%yqjCZgw2r>^QQSLjhavb!@xW@#wwW?%v|q zv&FG*3%5uA!SUulIF9{;gx=@abDv}1ecbN3-?8t0$ASAD#_wQy=sxmK3(l|dJHk1` z_){4ElEzo)c-9*feY3_t(0Gf+_iB8H#vjr6ZjGsDI-lFlj8sDMu0*&w1_%Mwh)OeD{k7}IrTU7a5HU6o_&uIKrjT=%Cu6qUNMYH!? z_EER~{Ok+0t8jb#vR~u&c<6miZ;yXsG;WWl&=u-=d;IpG#_jRg&ozDM_)YS+$Air} z-X0${fb-XJRfgd^!|*LohSE=_zM;6>hvJjN@U>z1{4m_$JjrD>E7o)zlo)oPMO{Gc z4}PiW+BzLgDvfmzCIoZRE_2<+V#zF7r363^XlSR_9+_)#$IXcNOo5 z<KaCt+W z6tnBen10Q2f6d~hm#W4R73bkO4zvK8970Zb(D%OHIehcRNb2}oqQD?pgUA{j@(_1h{m#+~XX%_`y2PG9#DJJ*Lp_p`UO z1ACbhyII#W#6QF?cjJd^AC&L&zs~H4zcDewvTAX+MnI%i2o?Mmr?OOrce@gGJaz4bnoI|Yt)3G?BR1JsVEo^ z9O3e8X~gy^eg(H-E6ppkGNp`v0hjU(neH_I$i3{>dYU}n<^Cn^AHUsU+J3Xm>kRcwJbB48uTGCJ zqgf06HMHVm;bC1-d{q>GSQkAR9|2$7@WmB8ljIEAyz@jFd~-c9c%r|>vlzxUvbwY#T>NWJxI(@M6Z>&Idjb%CdT?>@Rq}mu@$fQPqdn0-A}IR|e9(VFu$plB zHhM#f({vNo1}=Ujvfg_l-mU1K&@{51D{HW);){YC36t;FYFNiV+1sV)4t3CFDLR*? z$>8jS*52%gyutY731oT@^7|O|Ss!Io+>Sg8-WpXDJlT6fux-M`V3d@NGS5sITIO*L z+7kC2_WZ&wk{jtZ6WG%)CM~N>{E%Y$MVjr-L0d#tV+)Po!{jIOx*H$9P~O{Z7s`8y zp9#IlP~8Bl?QYZfkhCG{`CN_LZ5^v|Y1Vi^<++FQokD&jej)NDeqpb%4~_8k9_k=IZBLd) znNmKHkBNkdY>3TG@H&kvS&6sW)=4@dC$g+|(+z5P|&w$cvAT?XHN4}4biR~!5h zUxVY=%}-!6w{8CIi#qGR8y%jHs<1^H;%BhY!8yf~@JpD8UqUhZbpm>}h;O3{@lAML z`6DRZ*b_bcN`%tGpJ_cT{s5mUd%5+UwAi(~=;5D->S34C!xy^f;iNDEThmL8shZnJo1>ETKL1@&;!N0uIrrmR*yJ{XMu zQYSqefgW~tqlc$l{4Q7NVfIqty*fR(Buo!SYdt*qGJ4o^2|YY1Ob$(zdbe zQP_g6kUeU|UqW=3?Sr|AK2`8N#EW0$-SmlqS7^KrA4nN5*yEXM^s?Y%h?lWI4SlfS z@fxqvdfDEqNc3`sGY-8Ruk~^i^@v6TIwhG^BP}B{RE$-@reWCO~D&A-qZRepo@iH zR{f;kiT~&s)$dTh^yuK9`HtGI<0nI#y;9HSzD7kJdkk28P_N)%#tr8=3#FA`cwf{S zH5FgINx`-xSMbyWCBYN;(4C_HE~4IYPRohu2fZhIJA#H~<1G_=ZDvd)-zWA8R=EcL zOS5aBESa#^hx7>%9eqM3eE!eOU(Q3Z^SAq187H;T$F7IxnTzoi>^(J@$rwh)HK!R5 zo+R8F*JK4wMnwmO{+F8eBz}c$_zD*GahNiGswUj(6Nd$kqw9qBG-Imcnm&^;R5;xU z^r6t%`DZew5}GG;S_XT)iEK){wDFz9ar#heUk>YA{P%fxS!}9!A0MvuK%tX9R`{La zF!GtpCWMAH8%DnP@3GE~wV*jp-g~u&3I1KjwJ)3OkI??}uKjW$>G!lx{^+Wg*=xhG zEC+o@UM*3~XCfGr$bAy`?vw&mPvP@M>MA^rWo#p3l&8RDj1rfeWX|ATbVa-`%9QV` z&LQhEj~JMq85jiq1@SVMs7NmeB&H7w3}FAnKa!`+Pi}=)<|j2PBFt3qyTD~Wak$BC z%6#R9Y1Vut;*JQFuk9cI`&z?HC*JIXQ}X%)&r4oWORci3=6NYg4BxRP5nn+3 zXW!))aF{ZG`T=E=xy;t(v1aIe>1T~`sytqvm-75}RlF+Cd(g}L>Gz8ah5wtz&#vX2 zCcSjhmv0YeuS-;tS@+A2k3ZFx~;59WaRi3rrQl3rA-Ksp}r&;`Y zveDwt4~duZ>?eQW*Gn3IZB?XIK5!}D>9tX+JQu*FJTunCsPbF~F8sNf{DoitM0o^{ zT^g&(vrm_=p(ak1=lW?@c^+CGugdcd@zP$8k-zZkyvFx5CaChH>hip`D$(MPF3(@r z4p8NhZz-ibpOL@t%l`Iq!aA2K&mFpa?xk*3o^j+a{Hd#nu-c1w;m;cKm-hQ-jo-aI z(kdUgl<(=rC{>;>!G&K(SH-CE+yE~8`5pO-eEo>>2>!|1SXG{1=<=1Xi?jGO&Eik| z(s)%KdyecO|7f275PJC@wrN;`D$ihDo`;twTKv)Fd7-g$`=^q>eCIrsc-hzfcUQSo zdA_I1cW$j)l_!0Q#jm;RB24uj5-;!J9T|ox^LP2aRlY~PGBZ+@=M8WvPwgTOiMED{x!EO zS@o~8rdi{Kho%oS?XgAkcMWqk&&Yi7shcCs*b-N8GPul*4yU_Kqm(_l$iv=$!kx^V z+vPItxzgi4hsjz}@WY{n_)H&CensND>SLd>O1)3nh8gVFg>DjkFZ0cZN+tyDZ$pMM z7Ls}E-E)e9Z&42^$Ii4d0nsbJqa31F9;@^PKj6DLHO7EO$~GuDO7Z#@XoS}b)AIuI z?xavh;q5)F<;c721s7iKP9GN#*<8T$!pniRX`N&>_YTHK#M`p^v+u<#S&(m`MHc=t zWPn-=dW-y|i~|-YD&6;)E@P8FLCN+Y(wBFC!&0XzpZ%`yU7oCD-PTodwt%cX$(|0f z_Vmgkmy(S;!KM6djc!Xez@_{VD>5# zm1o7$7^^(Q3%`;p2P@px)w$?uDbIe=lkz;c$dc7}pqKJIA114*b5a$*Ehz{baiOo^~yTyU7gJHA}>Yk%_!?SS>(3~JZ7=G6Mx)GtatUNluzE(2HsWGm+`#t zYYe(t%JX~Tr91;y47Az{T=-+_>iOW(UVmLRK(&|6k55-8D&29K{G~kU^y{Ocm3OtA zcXbT+GL|34yDH=EoM~47UpFDb%p*?5xiPf~rhRVH-M2)V_wcUf6DRWclbc;;OM+9) zAMJhQH}b9?n(Q>QppiNoce>4Hj}g4F+4zJpQpJsYmypS~5RX%qOvdmT2K(!V_y<-o zZnMMI8xw*XJ?xLed#v{Hbzk%&6o0~_QEC5khH>VFjQ=){p_PoWLM}F>az%LhCJrf> z+w$R>>OXpuXD>T+%_kYLxkZh8hrg>So5P^7nL%8-!w6lvS0oZH*ESZfyNgfhYsI^T zZfMsf`V3hd=WFnAPrhWcO_ z$UX*?{NA+S{7pcT&MwvAB|6*(l>CMRCEYpHSJHWuLw3(hAyVfxDe zhSRj;FXcf8sASnqdwyIrjV+Xl7|bUOD8+BeAI z+%;&=ASeHy8)UTeT&N$!sxVya+!AlM;}0}$x7)oMx8>juf>ROO?(=`b@K%lcwB6;9 z;4sFv>l_fA3fp#>4Dj@j_?y5fPRCUohS!GSVmAw=mq|$|ek2T+xX}2EcJQh?+j_So zump>?wn|?N?yz9{mn>URU$?9-&{Wm5WO<;oopvGCTM4nu>z1vz8#`{eA=D=a0Q; z_dEwKs$J~H-;~|+!Yi&{f3c_+Yx2EWoA1LKeP8y`?8n}n zF3!{Kxvle=jf2k_5x(FF&f08!#o9OaZ~YH?Wli#gws-!G{rsXGcPg9b8?4V&(XWcl z^Y|9*LmP|KTIxymPm{F+vDaNmKQHSCud@!7!`?M3Cfz{HgFkBJd7Pq z<_@=m%iQ62X;}eTXIRa+L*|A9(}xCRE<2BS8PCSBf5P+J%NXTQsngucy^K2wZ*rJF z=U&E~%?k|E9zPylXq`h9In^qYYp@G@rDN;U(8~Bn?0nZ3MOpSr*6faN!B(`<8T{J= z4%W!6I`87l(BqFx2%e;_GIl#bU7zB7a5d*59hq~Hy?cW#ql=(B3Q5Oj(;}4g?|#C zLpg>2HosSB{z`k;ycNEdupT{cg7y7(H2ggYf7vfNc!K)cWfmK+&Et-B*$_SpkB|2* z3bwJ%_8LYF^^vZy++|E0*v_bHbpD?Yxcu%g%@X5&5zFE#y5+`Y2TQoOobTu=O6xj&tgvVr2^v-7{J9;|%M# zx$IjZ>-qMarV057^`m$Jd64=2BRnH|;~ej2=-leM^sIo)z19*hbFV804L0|1FM8`F z`~SVbz38m!c}~+_TYJ3PVZKPX=&DxMG(-mMHp)tOE8Vl3c#%)hJ(C8-@P1>XL-(9` zfOY13B7&zLa%2aR=9-bC~ogJKC557wu@bXq|Hi{eyrj{bTX8SoK|P4@^`%U5Gs;2K^J6 z7HJ-3UjWIs1^qKg=OcU;9@;#!d2I94_DjfxUU)1#4Q~URzlJj^ME~GRqQcs=D^M|s1`dX)B;vWl)e<%=|5*JZs}&Yb^nIc*tjHDWrIb3|AN&J+AYy_pFw+LeaaTpFfEA1w;GC4Yae!V%y+C zvBd>WZn=a!)6ZRej=mCEKEWOuopt^d0ow*8d}w_DUlq#IhCaNk%?Y|LHm8uV<{sLd zcDd$t+b4Xvb~d`b0v$gKU0;sQpNS91jAt%vw9Yh_cAdm}OfCHpb9QBGiqW>FSZqzB z@s*MOPjqqoRBP;(R~oCipI& zverFqo6Hlv9crD=wt;M0m7wysZ6u8i)*AR&$|HTkUiK#p9mjrF%~`MHXOCltE_JDO zJbT^skY}L6*YLd5_h+nohT4EyuyH7x4DnLm*BheLn!7!2mh;mjzd4j&#?A9;^tvka z!mkY;3%6|;dCL=2exK|5KF_`aYMmN-SvPzb8;|67>ohCBw;L=Q+s)8R`?&lrD?jKZ zKWA^ls;|av8es@*FQloeQ6Q= z7yjAn+6hY&RDS26m;4)R;?+9o_0UUw^RdxLU;eqy?}bGJRQ>FA(T%IDb#B{6dB~HZ z>T9oimt!N8{9e@g$FOcK{5=LP`7Nrk*8T0VfxT{R%g@gjC8_*u`Ptw}RrsakXBO+) zlHYYYzr>}9R{eB-x7Q3%^}B`oO8tf|?<_w<;J?)Oa`I!pACs}6Nq%>bp77VU5!&*z zswPS0_XF~m{Ho!w=g=0~!Au&vY=Y=B1A5N0#B zRci+DLUir#m`95&+4~<3Odk_a?=JKr+X*wQwzuD{O^exU1NtX5IxazcI9}-(Fz#1$N&jXL8HCy`S_&ce?4DB>!Zdm;5HxL|XZQOMP>{YuUUG zfy?-0Z!cFT`|@A<#jE^2BR!F=1kRC`{C}tMVg4wU-?cn1`9)y!lKl38OMWwIEPsPL zz$L%(#o9&%u5?BH04qPzllg1{b0ujnd%kpXp1YGQXZa)5SljlUjUC*%FPmLs>GH>P zc^c`fguh>qzTLj|T={Su{<(z9`1=F)p0VvxuT{i!*tVjk$C=-caVo#E7Sfb9zXu!D zkO@w6s4lZXnQa>ti{i>g^*Hcx#(9}37j0CQY)6^Qo4xCeqTq%;>^bH!Oar@=-rFL< z_;=c!npup*UK6@>%211(bVve5Aijt05-;By%auVqmn=(my7uUW)bmzMm-y#c9~FDh zbEG47r(G8GGR9YSC;6_Kq`mSzv)FqofnvA0)q?Y90Ok3sfn=4P3>3T5c?>FI2U-ji zyAK;mDY^`epJ(tW_Mwk~^87xavJ;V+z*cHXVY80_CBMTKoPQ@!*`0KFi4OMxCBNZ7 zNp~}dq?17+0$Gbv=@kPdJ)Z^t9d;*QaS3)O&#yd3J#N?G=p)g;igvr+Nsw=Yu{9+= z$5+JL6L;|yv3zN4d_X>-cBhqLIP6gIb~`?%al75_(YP%KzZ0A~+3W5fh2dh@l=y>s z-TN1U!wY*|eZS!F$X*vmrj*^uj%Uu?$u3kBhF6E-vIZV%M|wI8e>Dsj+EDs1yOUT? zx|*Fj*;M{htxl5j-{0s24~FJmlb4rq@yabI7@aq6?AUR+85#LG{L2`fBeA(TIb($| zH+OXYXhq)jd%GnoEQ?u(VeX>Ysav}f=cJ)9_;HEd@K~n?QNbPmd|B2l=Y6Tm#>M6& zKe1!U)l<7u))0$c(vu{QTxl38$Za%RG1mXf*`0E9ItTSb`zR-3-EpB)I$f8uJDt(# z`Seq(udwV+$A+nW5;~vDE1pH9zy zd5-AvB@gc?pRlvzx||(r4$m!yS{{;j_#k*Huw^7RLF56Q+423JeEBCo5fj#9)H8su zb@}+dPS#h3R2l&{YuwJ19qMc@4_UfcHz+b(WmmY3>9RLjkvpaA3ig$K3c4ro$-Sb| z8JL{%MA(kJ^%D#2Q(6-S(Z-Vwp_VIh2`@@7^S?Lb6r0gtHb^)iW>=f(s--k9L zjk8Cg9mbyIXUIeB4#O%V0zbtTp~euU8dHep`AG?#*A@0rmOF!B2&sn zv!C%SzM+~y_~^=*fP7b_{B59Zn9BIk?VP?8wGG;Q zXkXxd&(s7nzcL~4NZOvV0%%V{`)|hPDwI>8=b(n%3!bmaU)+zeYZaSnHE_H51v-^vQmL2fah)Fb{!eC+1o3kcLla zxIx1cn(mN>PiVM7!)l;>?_kSEjn*F-m99V~>yIUdI$QMfBF-%=+8J|AEwr}WL@K$7 zM28rGVM<d+F2yi&wX8(({+@wu81H7zfEe08CLnqw z&KX&DrZ~bJLp`^Wo_r5Enl`$Pc5Nt%qdnsSq8BQl8_n9Iyj$_e$wRbd6Limqyl3*B z|08rwykiBvgg_$l@fPQ}ya3&&_?!N$;txOJ#gKm&3vh3W#0p|6-E^tT9AoKc4&VYgL zR_Lz7C-i#Xpg<~mV|%(cupPR)(#HqZE=@7VR}Kzb58Ybm4m1ujZ}6oA)*%mT({Bu{ zh3@_I>jQsm9Bkg8=@vrwx3wu|rf+cIJ>=mv_EDAh;s)C4@HA_VU&;KanKX^X=oSzD zl-$caF{4+ES*gQo_^#3p-_a|fBm879=9V^n@8pi~pY$Ho5#HK6MTLv6??HYBYsE%$ z^Z5Ulbc*U{lwQeI{F6TCQaVNY9FbG$b8ZwjCr_aelUT56F9{KRLocH;* za{3n1NbL&O6XNS;kMORt0?rv#s>V{VOo$k`TS<@C#21z%QGWcr-aaR z-$5S|z9A&MN{9QL7yGF{jd=;5a|hqsIG4$IrrPiYL;EaS_t<@wjAOqJy492j!sC+I(*M3wK~1v`?}}v5ZyJI3*^+e3~(eHTH=t%SCs&C%MfW zXtzUqI%T|)g;?YuiuI5)j61Bc54xx_Dv&!>`oh>iD)JS>e!a;${1xPV=rrrBxyPW<+O#IBb7^9oyI4OO{_6bWLbV?Y+xHYZZtZs zG3`1vKABBhl$Xbwv(kRnK{pop+QZ(*`HF75qI&_l=b>B4_$416?L`k3FrIlEU6_ad zlYNi(lJ_I%&294%&4(Gg*mT!2mbri~9N|lc?uNj#&<#%=85oQX72TMG-n$)nl<`fU z!gO;>VV}T#ssFv-X2-%GdDfRa8~x>s-+CO;BR!cVBhyojhGu+1#@*G+9F)GVOmyfx zbl!OMZpv-xrWgG<51lv<-S{%P_C<8&{PY*gywJS_-Hqto_tv3TeQAN0(8(`B_hZqy zYg5f1rXT2_8(x_b__v{H=1b^!p}7(IcIdu@?icAVmEA~s7f5eJWop24MH+h96S$B* zHgEyjH_*-R4NW)SPk*_CZe(RzVE70RJP^8kMR#Lny4ip&6?y$~=4oet`tDKH5y^qu zN(!@cpYmKY>mFzS+_W*Hat9gxhD0U=W)f!O(&j@VQv#uKATl6rZ_7a*ZI;Wpb4-W~ zj1w8Cjx!r*f6*ypXyetQR~U5l?P$|UJHERv6}}7IwTkY2=-z?uN3`P&r1u`_NjqK` znrgnQ+wlfu@CfCUXJ4irZ8~Yk^efWM;l#g3JHAK0(Wy5C((Xz#kI;_8NzdbmER%M; zaYVY3Wlw5;z!QON+~qMn%ol8$!Kv2=7W4e|j8&r9qkN<t=z7kiyqdJ` zBdy0$uMJ!aT}kTjfG0az$;L(tC}0gp3PjlU}ATk+IuBuZMB} zaNrbv8B?tBf!){FQ4U$FxBVyDz{OtRX5JvMR*x;RG;Gu1hk?zE-8Z@;O{2nLR%Ilb zg*+?viA>@I7HU|f;d%`nq^06DT(9Bd8vYW<{t;@e_rsw@^Z|vO)$oe?_9(PZ5dQ(6 z+ZP;~@_5+<@|Jl_j2e%|loeMx0$I#cC!)IxRG(0w`h=%K`h+?NS{#b zv-*V7DPGloJjmF1Q*o4e4|8Xmc9!lF%Au7$;WXnr8Fx%(yb;ShZ6;~vIwegb5U2Wt z0@WwXLN9d8Sn!Nopd7HzyLq))Kv<{<-TIs5VE z5E=M_KISis3s%u5DBlER;8tWn`h*H}{b!5|L~m^;|83t*GG$G}rkjrpNS|=4qPs!S z{SZC<$kHV9Jng7_5#Z+n+EMz1d6(!DY&vO2(d!E?(I=>HoW(NQ|K3# z(l1OSy``kL#_ku;%@&>2Crr_Of=#zNb)4!)rBATvn1f~}lU{P5H+`y27uqLSbXK1* zMfV9pw}O0Qcux~)s~MzM#JqZV7H7rN4~PyP3XRMW?S9}X@Ylc(14aMa{Xuv?;6_JF z{4dw?Z5qGt^P9x)H1mgT{2KX{^ULKYeZ@c0FKzN9num&<=BBI|v+Wk1XD*S&oI+r& zhC4MptYMpmo91ac4U09*0vaB^V^!a^b?z&y^LiY={@3fB^dBzz5jS&*h-F`{jl`ed zu~qsDkAt-he(n8@&&4(^{fVrX7Exx2mv8!ujHI#&)keTY-%(uT3S38jQpCJL)?@HZ zR_8kuGj1>DnN846s&)pRWG|=*MG*mPX45wz7qWJWZ(*Q@b<U(4j?Mc?qR6j)-UW0aKbwohc z&}Kl}oBrgrl&d=CyUGuferg7^G0vp2S=IDYWl`qjqF6O{m-AO-?jmQ>oo4PL^BkMD zqMCExn8VJ3Hia>MOxk!w+r&D~3g$2IjF)DSZX@Z=u8v_|5^K)n?7>9zej{=yYp3}# zenq#)_|>MpsXA88O=d$YbB0FNXN1=7i)L5H2Y!DO<5Bwh3ew-E(su;%k*jTtd1amT zCeDjF#+>?1=>8FXbQ5%MLpQ%VA&`vU)ojkNeLHQq8b`fE`g^Mfm@lO5?Vwvwofz1+ zHrbr7=td~Iml-p@fRE6-jIrjE-b=2eGJo{|<{HW7oFbWPj8uI_x{Q&TL!|P4+H|GW z$$>wONi!D|^@Z*#MQ1|y{=mNGzjDs>h4lZ?uidrqN9)kB>YIMj{uOhj3+Yz}x|<*Q z_?Ghz1*vM@W9PA;nt9KfzGew!`Dez}qKi_|KZDl{HW#CNzKe-p_~kH^KWq z!25aBV;=lj0PkO-jph`kG9MhN=7VX>2b1YvesP;<>NPkcc z?PqCMt3I`SNRm06vw~!9I-E7y5gy|c)EyQKgRKEwj zSL2K6Cxqr1jc?ZY8yerG@o4%BdH#sT?K9KcHNHg0dw5RLyI14GHU5mo7lXgSy(e`< zpk$%P?CnkO*XJ(Rto!=AGvx@D_c@O z&U9q=`tOe3z5d0~d*rkwSC4DIyP)i|$mr#t^-71YyUIRt^`F*+aG<~AElIi+aLY^(;wM=k@Q1_ea!t=LdH0(mJ!<+wukjEK0 zgY;7l&@Lp2JFF6qCc9j>mvM7)Q6 z0px0t_-@YUl6}MFy6+P48AFusQTEwPbq-U#oa>Xqc{uwrC=-6Gdo1YP3B>iWbcY4K z?~pFv=a(L{p!Wcf=j!)aaQ-$Rf7y$5c&!eXebMn}EzPo^mk$ZWueCJBg5FZcNPNFa&sosh4*zhCEp4-)cQf_mwseyPz4f#Yx220M=#_Tjwlv3r zUeXq(mx`|z&^r!DRxjrO@ppZBI1s*!EZs&`CX? z>U16%`Kys`*RGMzjodww!*=qv|-ij*tL<7;)i{l4_o@hfzEu8z0gmk1r-tmALi@%H=jrQlB93mt!2 z<2y9|YmM*Lc$R+NexE+o@dtJM`x-y0@i#Qys`07%dHa2PMQ{Y-jE;Xva0JixU!?e* z@Sh8gAld$iE5K#n8vFSN!Q~nI+R05QZumLcW66r!^Kh$Ov7)YaRLH%5MMK@fC94hp ziiNBFdSe*FAE@nwT#OK^j`LT9Dg?QB-^Dq$NB+Z&9E-A4Hm&hn=Z(m=F`U^EveAs3 ztx+H+a{$M7I-#RLpU5$`({?ij`rM7N9p`UY$8Y3me2j*&Esb@)$HntH;359PWAw%| z$_w~%{=#DlEN|hA4COD}*jP8(Utg8IVtM_V+%Y-%ehCP16YkKm&@=ZtGyTqS<# zG~vY-3_n-p+xF{O8-7LmeJ%g|G2@iGK5f|PebY_mU%hPE*d>9c?C{s_+xM<#&D4eJ z8*8`5x-p_Cs~Q$94~))UxTL;5Tb<@b(-GD2!hQR`b!n0w1$@)(R$X7Uv~F~DcFpo- zj3ycz!`}QAMH=%vx_ zLwY&m+xF{ulXar}d$nKw*n-ggv>R$0ssgprd;*s*t6tMo$9i!<&b+Rw_gjIX4H!NT zXKC8l#d@@nVHH=_Et3Vm#I;X0;NUu*&6Qoh=%CY6yDu~EXq%V+c5-rrnk zLYoa6UZ<-gz1yoAB*F0XO2m%HC2PQP#bA7sYX$X{vgz$nRG9qhx}QU5{#AyhH=OjO ze9bz&j)e4$->zr}-Z{P9nqRUeZC{~0bJ&3zUT%Mg)w+E4MA4_q*FyPp9-+x!a@~9s z`H9W&D9^P3T&x%FL4=r7dw6XJ#Hh9I<_P@eEUgb+mD^b(kk|> ztvqCGtv=-0T2o|fb@-agC9c_Jz>giygb)jwTXXLlbIn^bb%RhANX!zfQoA-N0ME`&|#fND4a3g&1Bs)G| z+}GG6{7~m5HOB;v=KlQp@$1X455M00dhv_q=iwK{FOpvbKQ})YKh9ffb_CPi#^-F0 z-@=ymE$vR!{RHtwB=ftu@!rJ%QmO66&1jFch>oLc@Ox`vB%S%R?fDg z9*Wnbm+Qj^QO>-ReX1p`=6}18-+q;Eji+xyTza(cHpjnRs6cc{(%44(?oW?3ZhQR8 z3l)F5a3Lv=bR_*uogQsGy4_`bE@6c_>@BCE+8i0(f7WSSYo!C99LD}Pk&Pcm8o8Co z!YX870Ou}#>PmW6%4nU9JK$NN_c_kJ5Sl+O7Zn}&{l;ySxdO3IXvTMSuhZD_w$u3N zIAz@BO!}Af^~NVv)NwfFSxi~Jyv%;ulCJ!AK~H`5dw8Rc=sJju{PpISkSpYVt&{Ho zjIE!#9MAq$+83U8c?aNEyU|tlD{?d%DVgEE9@Ps(ZJ5Yf#)4C>s8FzoINxz!+E5ubTTze#6K2>E}pwl@+@Ara;|C zl^NsK_p5Rip3g|h7**+XoZsx^z2!aH?BxBT4XXIn@^jJ_axU?gYm2CFA^Xj~vg-o% z@8(P&_3nh~g9OUlOxuclRl<9zYdn0+;0(rx;HA{bW!jYs&C?GjhI)%-`6w zq`_cxQCGEWRA70arY+50!WNigjnIs{XMt(rWGC+YXP#Jdp|xx|*vZI}2I9nu6%cj>b)jk>P3 zK$qtaTqeH#GV$zZBYk!@@2L0_mCE^5K(7o6dCyAE^E}Xd4v3B|J!?U48&IBm2S|TW zddz~}H-YlpQ44y11*A_YJz_!cKA=3e8z}ibXF=~SpggzJg5DiK@#ETVLGPWwYj{rl zr1;yu*n;=Z08-S_QVV)ZfV}^u z>1GRh?*!skRVrg1ptl}4jOQwV^jq08fH}Zo;8j2$kfe7ClyZ&NxIl1kmW6N6u;Be7 z6!N_6&nV^o7?=g#4wUdVAY-lU79HNK;c6gbwQTX%ljmjtC0~Jx9~v*UaBm4v%2%x8 z#{(rF8E?z;;%_zrJVQf6$G0JrBMCnW%;xU^Q24qV*o)_$v!HhukRfg9P78W>0CS;x z6e#($0Hs{_SVFK#TrB%2Q22fTD15KgcnMI| z3rIRUi-D4lPvZiG?->@pUHs^vyPuy_u@oSXZ_$;Xpz@!K(SCE}qx|M*qKJqhBjHRfs&O;SLR(HLTRor(uSM0QFpy)4-!HCBy0T72_@E+~tgN zKIhu)igNCB?Q-EG7r8Ui={yj5kpJ7F9*uH1H%D!Ya`OK^?$1ST=Kfsd`N$}zY*1Pe+hnH(Ql@IlA ziGf+6cs(@YFJ{jNZ=~`P-$y-d7#jtLk@kFyHdIWr=Rc1M4o$13cUk$lTcm-wj!J%n zHPeW4$$+#U0%6a`Hc33R{mDbFKUn2+AJEV57o1;)etx#3AAUo?0_Y2Om_Iz6AVxjo=BtE1)mJ1%j&y_m86rJA^!4Y!1y=nwUHthLk z0_E!;Ql3a~{yMG)xC_Nkh2hbp7aBjGv_f&2+lS&?Ktl11u=v?w=?QHp{k37w&kv*j zYZ(1|Vf5F9(Hs6nbxlo6menm^SX;HmU~D79nyjY!+q0~FOc&xx$SHdM@6+mb-TQ!<8WcT-M@TAU0@V9OKR#Gje7qmlJ7VG&&moXMQu4>y_`6|zpkM{ zZoX=uqy}L1&@NmVsPX$pU6z*BmipWM)ytPRk!jTms=2&&$;xGNxZfRh4a+6lD%@il zn*0q-oCDZxdP~9%3zVu|g4BAR`u3!RHqQUrhfS)NjlfvAE;VJVw#5yD+3}wejNS$*7B#4sTE6V$&VE?op2^v+iyuS7_ bggv1VPTO=+1IzDK{9*0G9K$4?SJe2wA}&Te literal 0 HcmV?d00001 diff --git a/quartz/src/nativeInterop/libsodium/ios/lib/libsodium.a b/quartz/src/nativeInterop/libsodium/ios/lib/libsodium.a new file mode 100644 index 0000000000000000000000000000000000000000..ea3bdd06ed454350ebd00d6004d2430351c4cc22 GIT binary patch literal 1140408 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 z%0A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

    D)7g}lSB#~_;uX=@%ilc+R(E;^e8>) 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 z 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

    sA0B8Bf{26ygR~_{8+sfWEcP6^B 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 z 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�$7S{q%KX|Ch6hSE7oNT23@ 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

    G@YJL&rY!H+R_(<D zeF_;F?C5643vH}>>=RNUD(ezo=Sv^yNVHjx~vaqK+!dv0YR^yVUszirU>Cm&xH zeDCKs20wUqY48S)VYtw@3sM+b^$)NHpVp(FG&xRZPAjLi+)98mel=i7j^SDn1h+>aG%O`piM-> zf@h~WUcIn(2JD*Yuy3Y8_vXRg$qk;b{;isa(dN6N+Ch!d4mtxn=wsMHM|wJfpJRTA zbO)ZHBj(;tbaRAlq0$7+7PNcND^t?Z$mr-Fk-VF_%ZhLA1P zxw)k=zUhYsTWB$Cp&NqjiMj1(JLk5yZ1A#vVS6ai(GJ@T{x`#J@v;pwSd+KI_ub6y zf8S;zdr9g&vrW_w`n*VI6AcXB0h{RUkz#M&C{egJN9?6~Q`;RT`v~c^dfC!p6)!p=JJ<7D_@%=vNG;AfBDsM$!nOg7TVsiOVV&V&d% z>0{UoWE+s4J~x-_BpSEbPHKz`e(t640lV8ZTZe2CvJIrpCVNGW`)Z6Jxs$TkjxcS^ zFQxdfm7t@|V?`@$rL%95Eu`Xy$B7oyt+f<3!#}P^IN5PzE0LWg`T7Z^uZ>;(cb}b1 zY10@*`AeIFWUsZ3#x}A~mAxe6wl>BiO|s8=?LKpAI_ga0p0YE@R_X_RL}^kyvQgx? z*8;n+buwgh;jG~2?@WW8l6uhR>UB_#sgh^P);Wx@QiQdm-jv?w-~-j4`YG9wpYqsx z7UPvHyKY=Yz2%qA5lTC&8JF)Pl;Cpi++mMG-fAF-QxzZhcZ*mgatSj0jdG`kkK@-D< zIs^Wpa^3^Jyd-19qR&`Uc5b_&J2^q;}AJ`e(AEq^wH$ zmU5;&r+_EYjta}Dl^yj4+Igvs9d$R^QE1-}(7u{21$jKV6KxC|{RH@-wecy~$l%Y3 zGikxI$I-SZ?-{i1sV8LHk_=J%l8$z~cCqjv?5Uv`_uuLr)@hGp?WQ;M+qddO(=KQ4 zO|xCSH(fT?6TIU8WA9zytE$et|Gm#SNltDgXn=qy;UXkJxCm&hFrFk_LO{Sui&fJE z0VO6}ENJZj8VR;YQiaOQXNL93&U4m%?PuMd^{n4ooAsNF)hAyjd+5Eag45>sISb z#ckGDSJH+YBbj*}8Odcd&V$~?{651l8*et_8$VFv{awue754m})c6l(9AT0hzXm<} z=b|^_+Kb-k^Yp}wRBKX32EX%p`XX2L3ze(>qH{#Jb+eu$khPF+gr_aYwR@1QUPHF} zh<1Lxd6_L+eFJ{VS~}0&t0FeTXT`H#5I&xG;TB}3Z(B|9rp5F&zW2(l*6H8gW{G?% zyv%^V3Gnf1=yZ5F++{Xi1D`Y>i{ax2_?Q8Y?=xcwlhoK3x(7ZAFF(pfPMT)^;^P^} zNwdvgc;Th+wDl(3=b67~yA}7s5c%n^##iJebP_GBo{c@`oo?7tqL&ekA z=m)nWuYSP#0>#nX)$Ea#xxE$Wn^HvYlFjdTl)Tc6%yop{XOm>SkbW<7rZcwxWUX;V z;{PPaIP)M>!XAz%O~(B+)0n^>X-c45nPE(L$R1{lR2y< zDLZHnQ{=dTq<_9LTx;RW$ap5Q9($&%+}3C8X?}>#O7=}ZR{NwfX5|@1V=;Ye{Sd>7 zW8cnaHcS$l4J}(|)R!fcO%zwL%Y?;pGJ&+o?>agFgq*8Xn9)Y|t;p8ePN zL|5ZAiEie6;h*SW9R11>HRn0HqrV;ZSLiFvZrU(? zxWLUHjJ8VRbD;SdgSkNt&LN2tg`5AYi$pomB(1N!Cw2tHO4W1<(n?nfpLw+ zj7{4ZqcZp{V_ClGwtvU5rW?Vtc({>KJj%@I#Ao*N*ALAlzZ~+*Ccof30@DpmYyHq^ z#G6XIDa4x`RJQQ(T$I5bwhWGZ+G|^<@`d08bUXOu&X;H>Chf&VyP+SiLMQDuH+^8Y z1vk1Cmn~b0{E)hEnq`dfwLN&Xug%d3IkI_KbPRl*0$-=v>z=acXzY`*Pf_+L?77${ zE4v4K4)$DScVm}&$x(I}b|3a^WjFclQ}ULRr=uJ0-MD_JVXYrJS*?RE#V$Ja$-GN0 z@07#4WwQqTwY~ln9en#P^I&TsI@Ng|!k~L!6%FP?-q+38<)Zx?+Za1-e1TvLTKb@C zxAo;leINAXjA@%_W1Fy`P3;^oGY$zge$&rIqj{8_h#pL z!$6}a`UXeW;AO59osQ@lGMF=${@pZdubHsnq?w3Zl0chFwsj08X3s{yo?)Dkar7E9 z!>?Z$?8-RoN@+Si%$CLEHjKHS zt(poirofZQ@GBR5is{Fvkgje1b5iOzmi%h;nY5l`b5{H;{l5&;bh)FviMlK}& zXqKXBih312&C5uWDMGZwyQ`r)(S<$_U zZih-cUJRvcWzK<0Jeik6CiO#Hcuj#o5Q(Q}LpHXjt- zt>_bqZsn!Kzf#dsMe`I*SJb2E5fShSzemwpMJp9uuIL;^$19qw=vl@iiGN5@*^3r? ze?=1%J;(4R?k5x#Je1h?D*CdbHBjN>ZBVkvoD7wE^EtEtcFW-SFA$a)#=q1!G`dXD zB1L_QrYQ=M@X;Q(Z*rc;y|?Fqo^SSqUUJ})H!tzHH(&Yql}}vha#ar9IMj5l8(KNk z#eXJe_xiDwV>gcVxVMkqF?#1{8_N`khkBND1MTCKUC#%QFPiUTYd_BJR zq>zHAYG9leb7hFVkFp<7{tpvI^4If+EVKlF)vWwK5IYRhc!YfH0|NY4VJEuZ6o=TW zL+rUB_QN6e_7J^^F7BMQK@jd-+ch;5?i@Q=U&NiG!@7t& zmlp3BnTN$Fs(ORtCjSvrZ4=2~O-~Wyr*#uQy&Hdfup~?w4_%ZQFxc!+)|(yL$zS7< zEAbB(U*nS}I`KQbzs5UHju2mj8`8aqJCA@nkA^>ia#bPB3dETU{{rowh(B_T9+&?N z-ve>yyRZl1&ZjE7uID$1ABa1b_9b@Rj(8=b~ku)2@+Ox)M8Q>hYQ0p>T`q5r6iq2&9HfNl0U;k z%`f!4;x&=wMn`0{In}k?cp*6RWEO*BG|pVxSx$?-#y^0cFV1`-i+V8{XP$>%szMwt z5NGaF>7+?q%zfys0w)V}3qhtkoOzK-PmcqSlODsCQ3HOx(cXH@*m0pc1MXgG2 z2kD_Q5EGQ_RXE7p>o`j3gU>%6!YX5)_)9o1xOJ(TJQi-L3^0LmsQi{k#vRl`dM@YJ z`5Q;a9ewcJvR>%Dd!x_ogI+fk{cc}$-TlyYM{oKBY}LDk2AD-K=IFNtV=nlX8e}rY zPrvTF*4?l5DYyIJVRy_nO0Zcl*PmVr)*!}c`vlyy;ER40Wy}?fYpl5%ta-oV5BDw#S3 z%-VhqUOXN=)V5*Y1J4;}yC(+b~SZ5fw~-TeKKN(^(i>rb1@xl zb#g~A-3!)B+F)~x7rWr^1-C4C9>@K#Tkt|sA5vH0aMIGY zG%uZYCHOcwlPi4w$i}G2-mA1NhnIrsx7$F>G|DZwBIg{yao%09?0wc-t`)n7;dGUGSM9#;l9k~*;eLNmbK%6?Zob)SztpZze| zW1Yzpn`vakjWsj=GGj!lJw|*8Pg>xKgE_wJcyU|%-0$(eMeHT(u}t3Ak2h9h+DC;Q zV_NO7d#r7v@3ppH^IdC$8Mi^++Y5$R_LjjE*;FvM^8VmrpzJ@Zz2%k>sToH{_x%gJ zvx8%iaj^`%4`;mnnBT38_YUT_qkq@BZ)uGU**DAvD`J58{cP1O%q`zyj=7n+W*&3S zH^Jw9gFVD({~n?}C*{U4CvoQC&Q!wnC44{PUB;Z$pZ(^TO>*w(UFHfcWqHoFkbey*9%uOlBtz={FL8f1E_mBTKbsqm;VElhR zVEq4NOFsL{>MTX4@n7&djGH(4+PWV96;>~B{J%-Ye;EU0TyVy_X8-sv^MZ{3GL}01 zSjJhW|7#4Y%ncpmzcV+;SR-SK%nf?1@mL?&G1XG&i)Sd1~wP@4&w|CRp1~jkkW`PHELV%t>_QyH_3Y*be>@^4(it-E!#rtNI&k-o2cZ)7F!H(Z zHBDn%1Ki5a6Wbb2UKvRGpRKx$x#(78k6VyIzJ)AuGqTD&t35X+NLK0Ue9{NVC5FdW zpE5CZmaLUs;8@-L2QBSw-0|ZH%FV!SNdL5@*Ws2v#Agj3nQiq&2K*X*YMyx+IOtxD z-&$-~^NLK%=QZndlU3W_uI7ftuG;G{_^Yw3Uxi&TsAUR+`XuMeIxwiYU^{cbpk~|H#!&oeIP7WQ*wZ!c zGza`>wv8JVyy%8eKIBzr9MfwH9bcD6@^`}Kh6(%S;Y-8UmEbHLoN@2lrdURruWkNN z!IX*|?8q`j(NQ*b@^*z8EsFMF&#`V-n9-tWH+JsJQkc=AXcu-Lb_X+B6m9ZbFr&_x zCw1-UgHjq7gBk6Fk9F?jLirYqYwU!3Bq2Kw6BQk)Xn#fDqP!ArtD>6~U8m?uMHebMSJBCej#qSuqA7}c6g^Af zB>f|bzNzSLMb|;$!^|>=PFfD73TBo%bkanqjVptqtu6IBwBQ^Ter6tm@+z6Tp=6m^ z3q_xtSq2rpl`49hqB9j0T%h=mR5S&O4mVRWk#x6`sn}OTVR2?DRNRx5djp;lf4ibj zC|arLYDG&GJ%XZG!XHqyLD3zG)+oAB(Uv$T{F{pIR&=MLTNT}`=xQigWhN`TC;%kC zLyEqv=vGB56%{<7{T_-|D!N?JIf{-^G+EKJRGOW>qAx4@gra4No~AOTe=c_?`xpd=eBrhf-hcClq~4(Y=Z;hYBA?D!bqYCA?SB zR(hn^TNFK{XtJWL=WPE&ioUF<;0h(4;0lHA<)wsfSF}=5!3&DJ;01+_QB*K2Vi&xi zP{9ideOb{b6fIL!_GRtxj1xkSDEg+NyA|E3=o3(R@0+1yW#a~g-#+ZZZ>miOBR_7i z3_Dpw7b)sfG)+;6?XU2HhZ5dOXio6B4<){p*qjI|@QTVSHeTU!l?_-oz;vCsqHKVR z|EI4oq}+k~)~wxv>}ksGqce$lnMcCwadVrp>v8xKvGYPcm>@>N2HK^NVxOb@SE?|2 zoPAsD)LD`8XU%Ej0rkGo)z}3OsQvH2PLh6u(k#gSP>6kWh`lky-V$P$v;)(p8wS}s z@PM)t%LnhAk4oy@PzBSI{n5_Yzli&{fk3`HB0>Ko_LqVDlXRAR>A=4W-M<|>_S!CS zf9e)v{|0X8eY5lxW3g0J8V^RU0h79nWMpqp%z8z&oncVsH+w%({Lgp)HcJ9yBt4q9 zQNuqw=}!B%UEdF@(`lB(FbyO^b2kL%Eak#T@_us_zfyS|M@9~{KPq-{Z{`P6j8R4+ z($|@;{J1;`Bafupsa@lCUdE5VbMAvJGZjt`oq>(l^EtG6)vAB8?}b(PJe{_Ta~LT` zkWua;i-_sukL+NZNdEHfVj}sc*}jqe)4TB>(~bXl{NZV40~EGL*C<-1Xpy2mMbi|8 z*x}`0;JB>ERoy=72^WYD%20NRFQ$nn5D!Fhwq3thrLt>2mMFX8moZ)0)1YFG@<W?V33a3W!+hUX;c9eh2bk6k% zGv5@4Fyr0Q6*9{^CiGrJosMpYA_sS6y2F>GsdAO65yQ)S%e!SEzoa+`2aP3>#?MiN zl`(Wh}A z)@-mPtJu$D-|r*#@;>J79yt%*%DGC>naRGD+~M-eW!c=v6M!)(@7LO#Yiu&TWnT}( zP&DV7n-1en`8I2R=Qr-?2t-#P_K(;rw8NsycQLlxVb~LM?pl%iFZyj8!an7@>;-hh zckZ1L`-kD;OWs-!$8R&uNp}VN3tr}p#oRkxTi(^4=&kVM7Wncl_;WLSnunh8n^rsf zoPqa#hu+UEXHXyT89%+9JG$i#a((Bx@U9m9>7NE0mT6qd9Rg*)(`VRexjPr5BVTXd zg)RHB;qMqH@4uNkYjO9U!Li4UAGeAjUTQ({H@h-%e>|P zl-~N2_a894$G?)g?q;w31MpmJ^wCdfW3BX2xwFAJ=WT5JSttff`e(WBpE3S@gXVJj z2<~>BDSzj;^d0A38?m<~(LW{Zf`I-h_5fWsI-T%g2J7@Y`d~9|8e1cKrD9ii`EUL= z`oiNbW4wc@a`foz16~rYubSv+<^G=T_5@zxE)9**a`xgcRpW`Hmk*b=@3Z!~^6vIn z8S-rb*}vWFGtLZVKSKI+i`!^h=W?@O<%YLTnI8+dTc6+d-TLdfTVKYV(Ee@b@i$c8 zE$+TEfIM!z#ZBL)o}Ip}_AEl`y~Uj}BQ#97_Hwa(eE#-fsP&J3-NW2ruY1(`r}=@u z4_^1^^^#sEg@iX;Wj_kHPNKaBnLOmKaQbX<7gNf^PZ39g%ueNhUxfH0l_1Ex7dC#4 z@)vx6xco(rCuSi}xcoONf6=3bi$7B>awEmxq5PGY7bMF`zAtv;-_VV}d^bf*r1TGT z0+yXc#Na%ZlCUJhNZsOX=?o~LM* zqUlhY)XX6coz!2siym6imraWP(6h8Z+Fj-$sFdeThZYn;36hx(mG=-mwD_ke{{-kj z++X%M?k_sDU^`Ua<8kN^=z6HU$7*F?4wd&<2$gcrQPijCNJaZYhu|-IZ7HV*dIgmE z%Pz+ehZZ~my%P8PprVgYhe~$$2w9?n>82 z*Jf9YYmaBI$K#gInUq0KN1csojqJeE${lD?i7 zv34J7P}s|<(#9~Uu0Yovr5_Zyf9bU^*H@Am7X3KClE*C>v8lm6<&|qwaTu? z>Hi?U1lMLcdfqp z;qNyG2W9#$V!szqm?8YzSJL~h? zlZ;<9FAzOlzin>x2NY^^o~`RLTbteHrnko#o8G}+bpGN8_LZ=vqf3MayNj;g(e;bo z7}x7A%J)c}yM>)|nVdDbgl?Q7y5x>_D)+cM<@C4H z1%d76R@rI{x~p@bjgjw-&bo8E>AuZ;xq4?q>j~WoNAWrJgf)zt;q+|cE+&$mFhPx~ zdkDuCs2YWxIP(8ZHNh<8H$gi2XDJuf{$hg0XmRj$RP->=g3VCI%*^%B-q0c_b6@66rFWgh~)1Ur4p zzE9AjsOW7{a2LIe(7lSj428clUv%gs(cg%F4OH^o3-UgsROY$L*@N< zEBj6;L1yl7=%iZZ{y0?96+MoWy9_GjUI>+P=Ru`BlO0-+Mk4%uY2T5JaqTd7no8?( z#MR4< zTPVEf3v_!em-oW1`;XL{*mZxKr~GSh6LXXDcWj1nkRK9W_b;hOvFr9M(ZsIXzto%9 zb$^q4hQzM>*KhgFpWpb%FVOyfLj03?|3LqL5AlDFlmh+bd%J=5t3&+%ImG|n5WDCV z1LHpwVjmk4-Y?nPYr9VJcQ*=&u!|4*-n?Efbd_pY?LveSd``tdgw(>T1R?~Eu-1G+d(rlceLppqZF%tR)VCOcEg2c1;o(C9Knixl-Knx-g3!biuroEyc!+yu!VMPjFv zx*du<5NK}_I~D1Jidn@Y^S$Q74azQlVy@v4f6ZrU_hQ%Y{SV5n`HYwCp9U4PgGb!7 zL6!@$uMe?z%%q|tcLuOvx1d%MHbm#?PSSEN;VUg1MgI*>QO_v>Lseu&t)sjDPVtiB zmBsg>Zw-i47*sTMBXTet_=4{{9(VPGtHO?}b~#jrbQe9gh8tG}%VDJPV34i5Joiu} z11RQkRcYET&hqFEaK3uOiE5CN^dxR84QVXY=sd12Te5uqf|Z5$Ed6fr(uIYEGbc+h zZG0-7G*vFyzmxH(jLyV5U5qZ8ZRu_t+(SH{iX`Kpm(D4DGLO!Bh&%$vK}lF1$w%B( ze*C%sqsKo@`#I}&Q;mJL9p{$lwqxr=)j9PF;#(*^4~?uIMBytU9{ zo+-C=5{{n3Ig>6r4M)#*)-8Gx(aAafy~s=dZup})8(pBBd#~>mZ|r>K664J6e2c`3 z9w8rA$TxhneFFB{xYa)@i}o#fJ1J+0uYcO8`TYluYL0O&8IWz9N#hPWz8`~MZh)VA z$$$rqGpsA{%fWAQfS+f{fLDz(MPLB=E)9ND0{o(u3^2_zWnggmh7EpG1N@@N&pcDZ zJM+C8{H6u?#gLzQrh#`Ti=K*KUVxu>$ty1J@dh~;9)17&KGHtUU1GJD-2bD8UGWvi z%8gCjFH%urdYjUw_+B=)dPirQzJDGFP2*jeD0}!G{7GU-PvfU%d;45TtB5jKE^m_) z*ITik?{miY@@365eb-i)@%b})8Ce-o#+mtq7o4W>-KF%cMNc_8wz5CDF4n)G%u&;J} z^C;Im82oJ~6U=XDf4-vi@L(r?nUs~^E@y1mY}-{IZpK}W8R)jZ-Kngv`pfEkgQ*=o zZV=|(9mQTlGVjonce#Xd=2FfKc=-O3GtPv5f2p>-f9pr&Dfx1z$u?ak$KIjm75kG5 zOc6IWH&A~0t;334-zQjudq??i>r{S|e&z7*Cd&79%6TI?`8ix1K&mJOLWfS{(!63@k^6`a*M;G zUVfg{mWO}eB9}4zvsK^VJ@{t!yqkF6uhYId-j_7}M#*6=zk~Xr+_jw6$zRK7k>bl7 zC?=ABn(fQ^UM5}sl(?)MA#cfKj8SrACx5WwwyEU587BTZ6=M(n=>0mypQ-jUPeAH)2P)~d@&=M_3smgfm1NuZLJ5*7I4en4@PQJ)3@Yi~rrbq8 zC+?yblyoOUC7mp&q?@knvTl=fQXE>)0-weI5LD9J0k!jSXu(#fr1!X@l~4(vOp6hH zujqlrUJ0MYUIvwXmqW$92rBM*P)T>PLkpzdd*UvY%U^UecA*W5)+k!0Xpy2mMbi|8 z*zuGGc6-c@n4KWo4td}5HhV#~c^~&a0Tp~(O&njbiQN&q6U5v4gvx}CVBOBfwZ@%8 zv@`aTd7xfa_9=RLnPd#IE{_A?q-)>wffI zW!L@gM*PHI_Y;2FcHOQ&!Y*#wRA33T4-K(TB4D8Z@(}-@huD1~_7_9q|2D)Pc3~q& z!Mj+JiMMl#T`h_h&tI{)kQIF=kC4Snm^;>G#n!R9kx$*MToiJRdPorEtKl$wg=fif zgIm297T>c(K8)i)fx;};q;z@-7>J&6PS!_=px``VUbGvaPw1;_7*Cj#v{zuE?sUG} z6TVdbN>hrzj?eGd*cNG97kch~+|?TH5~9}&d=E4>U)K8bW?5un^!iiV#aSLbv7PVw zbG%wN>hm8u9@d|U$RRzDr;?Bh-QBD|T^F|gqzu99Pq}x^&Ew&Ts{dIW>(D_R z>&|Ji4&^?t+ts?%S%ZTdcw^{4M4<6Ox=wdGx{J7?1;XR$W8fpw54XdTq$x^tJT zI|IM5+s(RD%E~%MuaV2_vMb#Z?j2?AYTfC4%Qo;EwxZiQx2*H`%E8PW{8HAP@2GVr z<$aRhy8I1x`JFXc4elM~55EpGSmO(hC-82%bIbUKw{th0eV#P@dw9Ef5BlI>Spx^I zpT!=q?!;fpE`3wV9rzyhx>MeTIuQTBcM-RU>rQ{$3je-EtUKFxc_Y3rdHIc!bM^Ea z>aOMBPX1c{jTB$Y%aQ!kR8BjYZ24oA6Kph2BGS6^v`o?%S6Utr* zA>6L(o}NE=Nt-HA|1cMMe0O;h${sHBtN(1K?8EdB?el3p#;&c~tlxim>{BX*(d zpb}niEwY~UK*e4OpZ^Bdom*pSW45#IJm7uPdx-UBrFWxuGjwB|oUQV>x5n1SZfD(D zmar~iJ?qXBai`V85}c6z#9f7$C! zSyxLQ+W123pKIO8`l73Kr+9~5cXsj!S+sP$?v&WwtUD#h=U#V8-Q>RL{%1Plk{G@I)b`K2{*-Y|rzbxf7%$~K0W(pV5N*1{frwsCOq%LkHI!3| z`Z$C7=UIQsyflXRlB~?Z$;?}f%Z85^Jxv~r;l>;uF%m`|$w%DPT+OcwFnZmoX~_DM zlw|pd9|<^MkM$@2f0SYwwhvV8BzoX)*LAI;m*Oyb68Y&(!ioGO=aHcWqQ4e-Qsf7bCuMvR zxlGQ_i`=vtDsq$Hv?X4VLkq@0B|Xt+i@V^mCB4(+FY%8+CH|XGiT^TG!pS@>;Wk1g zeyMU_tlZ~9CH`dP?o;kVppsr1RMPwM_XQT0F84SeyRqorD-Pf%=CS#eA#)A<&T9()& z7&*@(rbUI(b}f&I3?$}!_XCzY!Tsl0ANMHvsk1!RCC*o;W6N@+d9x~~O1{&?(w%%K zjxaT|fF1l*Mgt6D&w_<+jiuqTK z(MPBW8DP}7thn0Yp~ve;{>dt6B!4}=NAgeWCVqN1{$slF&+5kC*NuOk@@I}0(}_Pa zzl)K!f$8Lbv*RAo|F&-Yi@Nb&*p2_-1b)6~vps#1ngfbM(6#hXc31^_u&HNMEplFSvWr`Ll>QgjLQA5#YC%xzfMQao- zQ?y7?pQ34sLL>?B;CWIs+@V)xJ4Or(LoBVUq3z`_-a7JO&XO2n980ZP0$pH*7CAgmYO z0*dGyC%o(Ji|*i%(9$KH!!KR7bU_F@!ztCGg`ER)$is0Af#ORHin)A#aR)qKrxMm9 z@)Q6+7A?HcIN|bMwtUgjpjzl6&rV_I-?4Jhca|;*iZ6i*!+CU*@P)<+mxZpspaOM~ zZ`kn5JDq%xI*Oc1_)`!UjzhUBt6RCVaMAn~-_ye97u5s4s3s1r2h?^gchc=`bG~w? z>|cqI^dzp_dooHKI?J71o`ZN%rK1O#ImmV5)Ih=1n5T;-sT&RV@(tv-DL?wOZHCX- zbRg!6#yQ66pQk;N^(N`rdd|STd*}Rz3I4_nKRoCo*|*O%H-)~tq&tV|9Hhhut)^x!W)8@$(@|g_(;YYc{j|T2B7oIqGLw`Pg=)0Gt4EjCc_rd>(b)CyS z(ziQ!`%45{CG|a!y7s?!dH3}>SKUP|_fkteen=Z~+V>Z$%lWD<<@# zc0gV7N{)#nPb+sr8aLyDw(4lcA@$4W&`$oTj5T5;KI2&@f03KSC=VHig3RfF_~G*3 ztzryuVs(nI^@V%!510QA92xWM`#l&F?fX5T1+t$l`pz<_=sPDv#oy=9f-%qp+&#)& z5|?;KD1n4;fJ(Ze*N}MI9a>NWm2@{kCEOh4FZXpw{E1LWcZhONQ|`hi2`~B*WM2C| z4@qD2$&&s7hZgLHO8PsYqQ|{Y`L9&|N{^f=dP#|=^vDG%*d<-jBTGD~FG+tlRKhhl zv|uMx(%%Y|a0?yx(jtcz%z;YzWgQ^##w&M-9ljY#7SRog)+k!0Xpy2mMbi|8*zr8> z@mUi+?xKZ@J?;i`x48%X@DA5b*NfA2mGxI+wI+o&H_wc zLgy9q{+%2t8;tstntETy-UzYs5p1vSPgj4kK@d)=O()2S(M1Rycpvq_Sv(;r5xu zT}%s4IC&{e8Y%|9h*37jBLDm2LL0Wff67v}ROTsYfyf=K!7}$kMJ{+5D)anyMddy! z`b6e!P>FZ5LkmRCko1Nq_cY}$@`lI{(wBsuBX6P2P|9t~9g>g88@I9&i2`d*J-9@7>V#kgfbR-(?X&l74eE#4dh;{_{fYA|C|$Zwaxd zhuHUr*h@m}K{HcV^Shc)xJG%=cZ#`Bh!bEz*gTmdWuob<81yUP^sF0lW=qE`l!=jv zL%?|pN(vV&E-4H;suh@Z@Gh-R!T&OWLG=b~V7hnOr>z2~Ud~VHiS)d1Y~v$+w^Yg! zS#74L;V*nolHkL{NWT=L?J@_;qnht|&%^6f1W8W@g)$Nu3k^39cXbc2PKULGZ3O4s zmFaF?e_X$a8mY=iPn^6AMk){G{I~y5;VbDBdNS_m1K0o5_2-xu?N}Jmu(Sm^b`-$Xf1SaWHnGhxFKGX26}G%OUmf z_7-E4E=P_3eZt@X?f02R-c!-XP2r4>bGO71`(1idhrP%n1^uLR7AKtkCfxaF4WlN4|4Jf>Q3(f)QR04I zH~xbA6VuGYc-N_Xx2Y5(`OE#YVrbi9I^{pP8-KaSR!pS$bGz}stsDP^-T0SwS z|JB|2XW`EvVSh&noIvRi?9hT#=;gS3p@IuI0O!PiuR{x7h6;{A^m$a#%vy&|s(~_} z==*p}*I}3R1V1GC`z$;v$oD*1>UDfk4Dvn3zV14+EMppuWs+x*Ski;1qiC6; zMT+_qO;glRw3&4H8Qq|0jiP0W7AfjeG)+;69na%lxVY5g?w>XUx!*i%wxUmb!X+yL zk6Ts)N4O@q!L=LPR(Gv?yPHonx%YxaJz=jBJnr2d=hIE=qSi-MM#Z>lqqfVH!chmJ z-h^(Au8rQ#a>Oul=)r;Oh$+;&tRu9%B+@*`a_*DHVBub)!!^*FR&+4Z=l^`d&5 zxt25}KRvGePNioj&cC~rU5{J;uEOhaVm{>{h~GRDVwZDmf&S?s_FsmC7rqCEKNMpB zd)_C|e?^G@gb;uK(xYqnziYThJ z{$ayv=etNhoHD;MkiCeK?e(C)0EV-w2Wxz_j@b>vDy8Mh%*jVSmU4M|S z8;SzV=vJk#&;cbLbf!-GJNGs#2rQ#s#QR;YPD`6GW-K}j2S+n@%ozJ-m$7578av)b zbcfvUvS^`=t}Xn|qC3Wp@v9@k>LWx3GSZwOm$}>!>~bXYJ-OHjqtDgdr|eoEAymxy z;%ZW2I?|JDYaTI;K~kPh`vRrKz_I3+ihzL$y|+14rE|06ZTOfNB^hm)CW1_1p-OM; z)wX+RddpRMV^pliNiQ%B*&_%vqTlFBUs9veYhB~S+7nW~z_jFdVd3p7Rs=W}7EaEd zo894{`R;J9!(IOhb?;XHA{Xyt(v%*jVZFn@9nY9XhLdPet0*j7w7Bq&@62Da$ngy2 z--{cZ^coy*qxpwUdaVewA}FDM5mC}_y3^;RTc#oHR0@H-YK%c4Jqw!rQ`0y^#RTee zGI5s&dDO_~39L7)=Vj6jUz_(e-;Xkie61P3&wXQfgReFJ@YFX(jLB{-*_Zvs$OK0@trKI5|+-k84Vmgn<7yXB3${^8E&E2De|D_>jrBjH8qYj-{$H^#Jt7q(8U+`~PO z34EI=5$skE^dm{=Ns`gg^h7u1+0-0kw6XT=(95{{wGJQYv(krTTZj3!`_pV8(etOMo)&ICAwz|=X ztJ`9@tMbi+x>Cba_0F26>LY97tAD*_U-iB2{(jrQTOkuj=tYpz0QcX!%bPe zzdC1ikLtCC%eL<(+QER-_Q>nA{)ZK=CjduI+8s4gzxFzD| z#ci?atsCx2tcu6&2Hf)Y?RhNU@YXFc607oX`zdbQ2$x|-)txkxt1`?Ub-WWau5O#z zvnro+V+u`RLI(RoH&9Xx%6=TZ1Nho>ix0bd94Y=Kwx z@M;L}Fk0S~cZlX4PMV2zkHXt}GrI1S8C6vduhzknB|1Wh@ zN!?_vIaob*P0#ASxc9^TKdHO7sJp*g^DFXmRgc3x1NZnf`>VZcdQ{I+b$e>xzQ^h* zdzoRt)99*XBbmC2s!DPt)g_NFt7^v2{pS^LB<+9wu}bRav@5l$((GNA?23UV)g`&y zRT~L&2A-YRxA(ExZdctP_Z3yA%>H#b#${C}Xd|iL&BmSodH{Fb_YcwydN~a#C zoAFg%!ep39RdIw#r!Kq`dR52r+q*ZfLh8u7Kd)jWd|bk_6doq=jJZFhI_>^m)j9BT zFzqsfb~%}Mo5j0bdH?IxL+?+jevNz{ftMNZazDIW0IwFpt10k&Has7Ce*#{mD6IHRU*t&QlzG@6%itv|kUfSev*A?(Ev5x)-jfJO12QQ-> zUbZj}2``V1j<)vjyCpZqI+hD=w486A@f>#h+8D$)HxHI?khyF(v9~;vXB~TPx^-fn zuPqV&kK!2(|D`?O%~J&bALMZ<9+bn|N8zpK{wDfwe044TivCKul4u9+s;8){E!5RI zc)U^itjlftyH__cR>WgB>U@ODp?~Ds{llr-Vk4IJ#MnTcjHOO8Xp1)z?smd0Alz=+ z^RH>|j3;)uA*=UR4_uv4J>M{GdpY4M3AcncQWDVqJ@DjRc-bQD&gHf1D;c+DKfPik^eA)i4YXlBE{~xN>v4HL?Knq`iwkJS%cLFCPNQh2 z+nDpD9q0So@m|_-0^`Yn>g9p$SdGuR9S@@24W`|#Q{!~0kzlvS1&r5sGhWM>D(%=s zIJe!7U!xtz`rGlsHLq6BUt{{)@dCn?5H68++=F&JM2&q@c()eX;xXo}I9FVq%z3kM zOU6y+y!G~+mt2*A+Z^0v&KnLt=NQTMoYxb6#_ex>OvWl_u8W7SGIqwn&z>Fp?B(!N z_?Ssr4?}0d&u4j_gO{I|pKE!qa^9tg%7*gk{$dq?{exown|b!aOxZ|VLna>g9oWZn`tSr=t8Z^;;- z<=NqkVK*>--iTWb;Y7w2dEsHkPcQ4H4BDs26B&W>!}-ix%vmBs*z&@~d8?DWaK7_a z34E8iO5{43w`6?P^VV_3g$%w6E92~=JR6n#Wk5}P-s)IWOjh#im8_qJvVPjf7%A(u zdSuS!$WXhOi*_^SO<@kYkvV9r%UQ1tS-l6D$&1~rn@_j}jEM_ra}TMxtCG1()@z>C zjmTj^Jyc{^*1d%x=kN{ zKG!<-!er~c(Z052!h4=Qtf4m{^ZR(7v z7k;ked6zPuvU$)@T?Vg)(FVuSwwAF@eV2DXMO&--tlOtT#iOX1LNZZb(Z9`?eKJ`+rb>g|nRvu%^ zYqaURsE3oZ^NzZRg^#h-xAQEZjcn!7b@7k1+Zwwrny8ETh;{J{b@60ST{KY_@&3Aa z2;O}^Y+X40K-a}0!rful#a`-ye&er;ebfbOKvft2NH}TN%jti*E=1OjS9MXs8d~aN zKD;fbPnA#?O|(&|i}|>VK1J%HiMIL({XpuX33pu=BV7r0T~xB>K1p3jJAc-6RsE1K zdl_dcFORE!l-~(^XI4A{9SH>!Z;vylUUI{4k9AV7`2xq8ajf~SnP$Dmn(yK`^VN(q zBfx@pv~6eXzBtZwwCyi6&d|0`)_JTG(za!s32fUJ=g_Zy4o$e&w*RVgC}YdTIrOhP zhn~7_D&x;ItCoA~{dK{7+A-!|lxMzFdFHxk_#I=NWG}(iU$b}69OLXI)Vgo!^ewxA z+yU{~svFs_oWs85Z1yi_v5$EJ-*=nIoeLe`aX35s6)Qd6XUV#1DzfK@tZZu$bTs>w zyP-FFeAYYAIpedfnYayNtvigh?qh3ygDe?cJ$|*j`UkF*GZJnN?wu9Uj*tfh!Ye79{+ zxqp{!-?_$P+jp+nZQD1K&O@ZR$Q4yLj#&T;*Gt#AG71LPLwodFHrW- zNV{$n>&MaTsj;6`l|=bpXFU^7`H`8co@G7o9`f%t){v~bt14Kti7dUIeDX|h)m?jk zYwPGsNE01X)r>vt=ZW4aGq&2|cLk5$S1%DA2lBG)dAHympJrOJAL|7NaCY{qwf_BY z&V+n&r;E1J`K}awHbKqb%$pGVyKt2 zHNGH$yfU+Lti5JJQ#yMy(|PWqOm*z*E@3?|kA3~^W3Q>s%$jT&=AA!IyF7K4m%V*6 z(L3vKOxYx(|G-&N-eR6#c*=g%W;VY+Fri@*c`j{jdf=bTB*MANCT%eLRv6@u4!Ox| zrdFUMZpwhlT^EgyxLS>bvPs?{9?S5_zMONuVeM0LhC|Lh^tR7c6uoV3dXVQS9$Cjd znPYBR%cH*~wD=OR7ObUTbNSlt8kK96UOm~G#k-8AY|Hn}s@M%TeK%H& zg__in0pD-$F~yo~BsGnmcun;w%DU9((X^lP&A()-HO5G8N_M4IY%+Q_jfF1yny;C{IN_bVIJ#j>v% z*6nM5J26-41)eOQYFZ1|?wR-yzZd*}uKF1hQ)kUI?)b5+tsdd`qx^RH%%#cDWLMvc zvA89=5}Qi)&8#>9o}!3)DWhJRupdq$kN$&a7;+sl% zH}sx%T@+sxV-*c5n^ZE0Zw-gsk@Lx&IsSWcoU=*M;4peIe)Xm;_F+6rWnAk^ynfu9 z9nE*5oO3+!u^IBrM(PkXs`h>EFCK_empD6p{vY+*DtluA9(+3G`&uPxUrX=9XdPn5{;Y%L(ED0?zbbTJYcS*G zSjHZ`uQdo=hwOjtW<2;cV?)ROY$Lj>IRCy@uINMvmx(?m!`2Zxd+H0}({a|}@viu~ zB=88!v5Ssd+(fT55*@ebk~ZO1fgA0vZYKJ|l>u(_|2oUoaf@zSbljrr?pf83FzM(L zy*+zZ_vN?X3{s)LcxQt3DSiK?H?FpRS>C4=eALDq&QUez@(r3Tu7lFw7LW3^71Q?P z2w&8%VNyKp@NmV{`W)JE0&TOB_|hgbX?xF7w%Lpw`xlL_evWbB6ypM8X+@knuBnZ2 z;Wql%u~ojd(bR1-V~Iig%`;;w9wu$ks~w6m4!-!o1gnnU#qQXq>j?j_E3Rogbh1$A3RvF)#E28ULRvB$l*0(9M9utJ>@kIQ@F8AQr?=#eA-8CxPDrMa0*yrzqZmUK86{mL1u+H4+;OX|*^GM%{ zql3Nnd@1u!i*f@q!raogqGedDCA#u-zR&Fy`%Ok$tv&bjuQ+C2RuS(?sc3m7%1U?j zt2p*twAFGwW!=RZ-tDr>DZfp|7b&xSx0d)VaOR#sKe+?I-(T$bPWwuyZF|k$Gq_uz z=`PyX@e!1rcAaN>H{4~$ZkTI&x2tkOP`9Rj}3>8 zfEMxn04+Ck!tbG1F5z8;kCjl(%UXC+Pyahf|4TI!E50Uui9R@yK6I@+p{d60I;i_c zt^4LIuW4Lj-S4|Lj{cv|H_W}4PtW(p=I6^hyzOq7;`EMtIoRyH)3CwoBZtlQmp z{&+1ESzzfv>C4Cj%t1@viD{UW-G9MSyWH;B4f5`MAALhQ@4Z;wn|WOFew+K+gva4z z=lRIaEw0|fjVR;B&sN>ST=OmFoST_@<}n9-6B+s&%umrl-$HBOb#Wwr9_@!tY9zy{PHT}g2qkA-jZMix3W+9H1hd2 zblDDWB7t$)UG+Be&^zqo>}I^*$9P|FYWz+#u%{weK_B~BfmlHo z`$xf8!5FZDq9YfqpeqC`Xx0r>Siyk`D>$0DyE9fWr3;+b^7dSsh z4jQEP&vTJ81S>d%b|pnpHK#q_O+8$cAsqZ*_gKLY83N3v)Je;<4$S6Z>Ot>MhsO&3 z@8!V92}`4t-sqxYiz zJDuy1`#bJHw;^YG#kaY98V|$0p8k3|-B=4Xfo2 zjQ!O&;l5L?W&aO*I}ek`)!@~x0ms$~CasP4uj1Z@wcOiKP54T}k6O(>+Ug|sXBw+l zQ_gsJvypum;g#ImknHMN*ONUNDaV+rH`#nKgm;8vKORUrT>VI=FZVX2hP}5z?oJuOo|l{_F9A<5opb|r(dl=4Z5%;5I0C_#932sD z)yf_K_ck0?Sj*;kpJneAmq)@`&SJ0k2KIbsviCcKJ>Y!yh^Mnh><-=|E*BhA3jVza z*PHNth?mM9F<7SDXzu(C{@z$E_sRT{ee_S*M-Rj@Wq@NE4u(nY@zL0&!!HvP^%Cau@h zI*!O#Cg-ep=$UejWzuKL2cajGv*+DonbiFtw72ucGS$(J(u}mqr{o;)Y);ZeE%l~<`^->^~$sLSk5)6}#X98RCcFffJ zf#8VZ!CI6MH-U6ChAtB)$Ky zXdC-E+uFl_4~fPrcnB|dyeww#-?@k6HE?dR)n0Yqz^@p~cCp94j{V|?*e~wDA-o38 zE!K~78?5%$n`lddlX#Eta;|YQynGs-Zetvk`x_D&f8;!O$2o1e=f$w^8@Q2hdEnOM z9+G_8M^D1Z8BKB9Un6M|2Qb;gz7##zS-!Tc-$pTzSv?L)?vmS?7O|4Y7g zqA(B_mI_886`f)#d%AtVh4s0J3+oma7R>kJQDB_uTZ_TF-Ua?g_#KGPduoe=&(nCM zK)<)CpFn$P*u`P7d>8BN!td+}uCt?@b8zc z>?C8W+$;CVvOFsv?3#U_9OJCqCwFcJ_ve9QJIXos_1KR+7iG0vLpjUAYz$UdJ$`E( zUxUk-&;o`!*4?`z-ma*zjgeY7-PI2 z>lc764TqyU3T7l@jqln4mrpNwkZ)}Uw$X?)*WGs`K5x2qKBRv;`O0|GRX$(#nF+yO zZXY`;?3szc^Ag*TCyyadZZP92^qGm{)a!ha2f*nIb`#vTIx7)h5tjl6UY(OjTbEOx zo&}yX3%7Nydfg@scs|hKyZWAmo9}MKA=k$9Een_b8y-RUeEIH1F7Y^fA$)7z0?r0| z$2jcNpSX!X_bD+Qd6DxhJ&dt1Gbp+0WFM;dyyaVIb$#j+3!Kly=3FQ5bA-xdzr6(UH_r8 z2J-CTybFDZ^Dghs%3gP5=|Je>;YQp1wGT}!TKoNp3)ZfmSUk&U>-M~h`@c{BIk)VW zjNK+1E8lzRi6m zuf2Wl?Be3Ar1tjlvy0!$dfjD=KlQxK z`Q}Xz(jM(fn$bSV7&`*{Z2V8TW4A3YUY2z_${7Cud7dJ#_RrjB7LbR;Yx%T&?(4YC zDL$2TntYP)y~57NaFLJCxVw>jba^Fw(I@S5lgMX`D|rUx7<-)W3?}ieQkIS6vpm`u z{}YwZ0nRWC__O;=uF9*dmAv4EvubC zgwN~iD(yK?Y3I_176Up>aXVU=HhX$xVG^%)URuB5nsN$1Ze1Bv7e)`au z;+Cw}&E$Cch~0-q+v#Lx4WNDLKGeh2W5yp;9~y=KdwiQ~cJWf_JMex~mh{;_s6Hg| zqz^rd+i3dgDe0^CTw&*PhJ3Eiar%%huY}iq2u!`*SEUd2;9aFG%gARLZTSI}&vWQe zq`yjAmblV~o`n~Z*KFF&{a zl|H23SNhOM$uFP}>HMS*`SYU>h09O+kdCkWkZph0=|j)}ucK6GkPzo2qRAJTbA zAJTc9q7Q}3OZt$cEqzGKShcL_>xSo83CPFwU(2;dv+mh3HQO>=Ju04zn`{ksnH397 zSH+1Kv+Yzo-?U9K+upg5@{^=AV2pUTs*&`1b z`&Q(2@1X1Kb7vnR&t;gsD;il#NBM^CvB75B ziD9^pFxzT57uj;X+4khxocfQqL~S~}Hm3gEVDBcrb+mD_%h*)*j=AYc)+1WRtKrO- z==y3o7gn?OR}<69*G{Ziy9-)A(Hcrx=m3xYfOY8ibLyLa;H|gQuQuA+Nc$7fn@Ao| zkrzKKM;_-njN2gc5w{xFJp+-;2Pv7ohV|A!e*66?iydCc?sj^l@it|4WKwrfdOpGh zrspHA!1R3N8<<|pu$k8J5wq;NJ9*t~yY5a;yTPuz!|`)E>TVu&cPn*QNZl>C@Ve9W z7Ou`_P*){VcXv`pC1%^vWz^XU>g>B_+woPnRm0~;P3~~Wt3SLpp}uT^XHz!sq3cNV zWNo=R4+y^ldHj{}6@w&vNcws&rVC-c4{p4 z+DUwPp19+_6rM;QZH{|z(s_?7{}nXia9b@1jnc=J5Gc>&(Mq zyor8rQrUtY;rUXY@a6I4ste%DpWw@J_;L!qd;niQ{)+hWDST;%FK&0+i5Pd=@p!k> zj|RBg4v&H_?<9xki#zGdZKtktw>4kyZhI%!-F9S}`>PpWX1Lpq&UUvQ zy9vI`gDUv4EV|6~+?Vnpw6;f7YtdS>z6hPh zu>oaYZ()5c>jTkmeCm!H9#_07>zH9o7!F0TG~uooK- z&@zVBFKQX1nf(RvUln54;h5J+- zt?O%@*wW8RLnf5^{1vjN)K!|28Q)g-_oN|Py^0)~0~LRfzvRB2qa(g$9Upg#bpvOS zMj@9k+jm2S?6YrF^7W@xUXj*_x1F!@8{`XF)J&p`t79KEY7Ak)l<%8 z$m^$|NBR_48|{00`cyn=m`wwXJ{9l1(Azq-tB)0H#8%`RaTVZw)O|fYD-J)C%APOp z@j^fAm^+)jd2hvO-o@aa9_OBuJn#XBXNhi7?mGD)<@2q3d)6SIv2>KJ$LwDreMxkg zZgiO@dd$J-JoTQv+>2tIrylcnTt*`2i;^g`%1s5ZBHuvSE+ydKB00K zI2;dtQG|&mYz%R{>{rBE8S?$6G1*pTvaf9v?L_W}Jpe}LZg5jBo^l?!yLTNJrl)y^ zs&nqofSLL!n5mn%uXrx^6+g^3WaPdu`F_el?mPYsc!b}ATX-If)Kg%ja=2Ga{Pq3D zU>&%VI;JWbEK3a7sTB&dbsT&c_wQES$z9b8xO&{wdojKqB_Bs1}d!N0( z{XLV+u0k32XN*lq+m;c;4zP2Uah6iCw+ip8xWb}TmoaGax)Mvw49(x> zcn4a4%#pMtia5kJ=xP(~jP_41en$)~4i>+`mhA&<+0fa#KgV9oLR^83ruLOT!8Yw- z>`SH;?-#?0Lx^GAUN3)m$#Jo0Nwgl*csqEX6PN8WG1`6uo%HzBrU>^AVgOt&eoLGw zHW7<0)V&G3e#E2ePi(puuxa}lHf<+N9dx`Gmb@jNU1AdN5yMW_n@k+MY+~Xi68|op zSa_!myOUVndl&Oa&5?r@2eBvlA^Dv`fNc-JpyLJlFBI52(EA{CBW?-r5?mS43NMW~ z+%k7p3vsj4X_uFJ>BOOn1Sh#3TK~W%WUifq&^nm&CC))@gI&=BLtK&Q?UF4+U5Oor zxsuT1#X#e{D8zJ`F4-?Sl!WMXnkw;%8^q(qG2&k6nhjlNl5=Sq zIhVd#azK2sB$V$V);$Hh-+}ibcrQZlUqJ82&0Tc7%#s5lqa;*3X71>(2i_U*8j9Z& z`-|I(A4B&7=)RA*;|~ybuYm7s)1MAj?8o*`&i66joT&KhPj6LB`R2D3`@h~-(cztv zXLfw~qXsYiVF_+eAIDU-gI=G~AAXq0JyXUF1U4Ml2pzAZ`-~E+kL_>q%N+^xCSv$s!x%SlUdj1nwc?6d8S1iZu(<4_>4))7SO2_h*KU(VOU}!2S=6># z-+QxMuM!`qU(gU2V@d0;wATiHRnibk`#bR+Eaf}c!_m9sx6q;^eeyf;yCt2(8%y34 zt|cnDnmW3FYmTbyt#)#!K--@$`9M@I=^*0h%P!0j)1YMr@ZT@_P<*(gGuNH<>pkH8 z8+gB15-VN==NC(UBk5lG2fn$tO4^HM^wsZ6P5_Im{5N1-utzE@X@~7q7q?~c2jaz& z_XTzg+*bt_=YEs+ScpTPKyH>4#c z%qE^97T(!)*{+?ewF6CI+7e>kpSjiP`})Xz+RAShHQWu4@m2j8t<$l4G|S=)+8OS7 z89wA=_>fgfh!%t_yhgEk&cmAqtKnMhks+Fl^OT~;cM8{1vCBJ79I4}Kw5O{nT${lC z_o-%WEimFUdVWuxwZ|rCEo!Lt3wVY_rY@d8(DsIL8DhziX`1)weOf5jXNoh#ZtiF9}_Zg!b7gIsCgw;Y|Uy$LOdr>`|Bs&)#T2e~)n*(1MWt0ly>it=n=DXPhn0>83= zcUChMS0wOvfw!np#G(qP4dI$KQ*}uj8EG4ntn2o}HA`QUtI!mpjRbZUdl75`Ju?2; zxdB-&VoQ2b!I$YO4=y2B4 zVXUh|S#yVA%VE;(U-dP2-}md;FQX&;%!uLl-)$H1_l_m`Il9f-?};TTc~ZHDXfpN@ zQpYZN(Pf+^FR~K(t(==&|G?Oh+3=wSd>g6P_)xnRh!t6i+*g5rY#k>u5F=8?kJNSL z>A}ZakG(I-lFEJ6(SN3+9zkBzQOSOtz?gx4oxu1Zew{#; zrIk)#crdcDU}R&#$i{+idN2P9^Uo!H9~7meS7Iw?}JI@%B6Lg-c3p6 zD(8bzcTfUeqNBVW`{W%a_D@ZWPsZj+W4!K_`U2v=9YkNS7kxqFvSpcj(HAr>E6Y42 z^#$f&UvsSg{*6zX>hyc22K&BDOe}BYXo8+{oHs(#Cawc}Fu6y|;>c$2Cd=g>E{o$@ zIeRr79~@ck9gyv79+d4nJ1X0EZg{p&>SjI}m+iYSDcdLYGZzPE`!3~V`=pMhX-c;5 z&DlCGP={q%jXhcU)``00Bq z^u0C2H~pGEAx@X)csJSo_0HJ!dUA}%m-)wce3^fIdl_fVjHAF9zp*dnc-Q-n@&0*Y z7y6@1tMMMr`0a9y_P*-yayjN(_m3R&t@}sHDI}g=FVIIX&_{wka??kfj6T{*A8pU_ zkNaQJN7Y6j?acC>+>`a~xE~PWYZ(-RY%v7cVhFOukei;50zN0X_!`K5lJn2ad%il*yY5qPsmgVy<2=U4BJsll~{06Ss6*7)L zukOq@(7AjjdXCoLfG^Po&24-Gov+dPW@xPA2k1K-`i_FW_dwq%27T{`z6Az-$3fpo z27ModzD2F*y8`-Z(03j5t$@BS+&p~)biFB>3TXOEXj%mP|9=$}pZxz2{sH=#z~ zMAA1gM&BH`->=t=rC&O?>X+ZpFCWq`f23bd(JyEIPW^I@ercg!KBZs2pkKa{V?D@E z*Q8*7zntmg@0X;${(ecLUm`<|`94DL8y|fV=$Fr{+e5SV%=h8fjrGfk!O#}nJ7t0S zd@nR=J)i7lu5Ue`>}4I-@dLDc5Ph;fW=#IJJ=ovhwoQ4ibMDE0o%x)?^C}TXz;8{8vb33*K`dVwz-}BqC?VsP3rN;cueA>)>d2MXp zFi+`YTh2}T*lr8`+g=;nm#gs$;b;Q(H}!|aH~OQoO!5Wz2U@NZQ^Q^Gh>upX>)FU`;6ShL6gqD+&*J;Yg87_d18Y(B6#2JA-$>|tQ@4cJc&*b-nrGGNOL*pt8> zHejm_*jiveHDJ#eu;+o{2cVC&N(1(b*o@k0)A3Q*AQq|Cu(=6J^r~BeV%FH>+7zkb zqefwKCv6vRy{cSl!d}?>izyk%slvR}3+O+kE>!A3FQY3w$Ua}!g|^axO1-Dli&m{& zk$Hvv&UADmzUuDfUxt=W+{3d=u=k!>^yR?DL~NztaU}2E2;NC~ZRj5x!Iyg_6TFWs zmwb%MVS-20uQ{t6fpS^Di8}hd{ukB_ylX)J8hBTkcSF8Y;9UdySREgF1oW}fLX}H0 zm*~m$Co=vLUH_}ksrg<#)8L!aGv%0%U!Gz^HijR})Qu0kK&9q40n^LvTB7L`w9283F3d&Yy-ii$QP#2}mU^Tg@Irf*G zy56E-rmZ|Xg8Q_FFI{FTmt_H37v5NI!hWw+`N|v1mxW$eUUOr)$#z}&!5hn$g!+{qw&CPc7(vdHkuDjZUi$s5!Dpim z-RHG@5R22qbmSK^r0fD40~!0i5!=Tm>>j7+qumP6-)`yXgdXSCE+%;0~>P;e6aGJ$+et^{_eU#SI_5iJ~K0cbhAB9{LzoQ(# z-;tDs{$-g|=<$HRgaumJbQ=cpl&DT$t&AD3_~$rxS^I%6* zw$Fey01G!@rwrJqz@iM8Io5x_nAm}iwgxQGfb{{^!GPr$ut~t;3|N5yTLdiLfR!1r z3Siv~ShWG$4lLP#?K5BvzjP|{0n0I9lYnI! zumS_N2$<7=l^L)KU_%U8wE^1>Y=i;ZXTTbOjW%GX4A`f@#v3qmN27fm2RiZ$SfT;z z18k}R%Q0Y+fZc1r3JlmHVABm)nE|T+Hp_rj8?f!b3Jusk1J(d+t^qq`z&-`G(14jc z8SU#d(D7pfmT18G0DIJc-b7{!6GVmRKyT15+-M(F4{PsOMzP`Tr*Pjt#O8FNdO7#6- zDVM&)@1X4KZ}Q}v5zpKq=bcjdpZOz!`D2xwhpMrK*oob=gs(8^_SIgG;`;)c$+k6d z*4Jvzfwt+_>;(dC)34dX1=^-x{}a>=wD&(tOWFS&*$cc*-(?s1{Cv>gso#=~ej7}` z`HX(6`JR|9-yl!A8C#LdQ+~x9uCjic$m5~3iqWE!TOTc**f%~!{4Hy_QfjKZ6+L|G zHBPR-O@<%l2!d=203Jw#dV!O!=}{DQi2Z8g6E%3 zyQZV>m;+8JZJLhGBb_`l_zNCqFR_|@X0>y+1i5U$G_Hx!R2#;9Qwr79KHCz2s@?p#WBKe$#zw94%nh>H1dRpkee)Y ziB+_aXONuU*p&G5+s`6q$`6T|BJ+E1!|s6CPjqwT@P=WBFdRDs^1thN4;VI0)!1yvdKw+T2_u@W1YBjg#Ab~5XLt;BX_E_MOy z$S1xM*d<`+$rqkNoGW4%V3QT*&LysvKetqLWiGZ&so;&gB}UInAbogNaeV|H^6VxX zc7|cDbL5n=qx*|=4slJuX67YmTn&xi=J<%?W9S>YBu3;L_ATwPUop3`7cyZ(A?=g0 zdDm*)p6Dp{M0OF2olp>VLdy;Nq!ZW|?Z>_ddbnQ(?|tmbPGDEI(}=xwl>GG4&giw` zcd_$|BCn6%-syAjE`v7!yQhKJJq7HOz5wqGcn8SQDeJYz{wWIkr%Q~17V@UNY1lnY z;hT9B8qXw`CV7OgG3!t{1lR)xtPXpl_4H9SbQub4%#ouNCl$MHpEL%%mw~OPuFO%H zW>U$s66J1D+gH9zy%ut*MDTsKU=PyrgBT(EZrzb_+9l2lIA0uYakaodUm#ajaEGBT zQ}{5Kf_>6U(D`kS1K9WEb3BZ_&ra;VHe&bHq4)?H9cPF0@C z+*N6&Ez;IWp&d>|shokmpThkzsH+@#vqCA15pn#ls&{IgIe&S?seRoN?E3Olh|5RL zF$-f`?sH_^DQO!roo__$eX^KSmEr_p!v>~``LmJvlU#&at{SCD8<>Ev+IjXr(nqaE z>PR`uJ<(b0{buw3d}5HRIn~H(whz5SfnScH@71ei%p<>VQ$Ww--^2U$k}I)yn$0n% zq^%gm5zaB6dCXoyJUI>-pKM*p@5R$4aU#9suyB;vgi_)WYDt)&jf}lWV!f%1Ii+%8 z$wBc@iABUyo=Uk|a#Yw$>>?X@4)9qeABrEAbQZUj91%TA!bK?Xw!jB1{+-BN+)3P3 zQm;R=5AbB*9ZL=gYl&4%E%}|8R?t#C$Qe-~zs(d{f} zs~tJZ_+*ilDbmgoyp;S{eGZd3E99K^$*-L*J$8*6seSV6EY}+J-#lwuh#c# zslMhj z^wl|h9b4$DPl0_1okOJ$+i3bKw61C=slixs-5248)p~)$OCSL?-(w=b=_T4l=CjAc`Cz1H^#o)&miyvQS{P^%U zyjdS8>F+OXY;NZJ@h0NOmxLc*Dt>%v`0@3@k57I(_zn8!lg^Bji0`Hea`^uan#ku= z={~VHIoI9~Z3>{xgV3f3+Te5eccIN9X!8iPc^ul5K$|jyHZPccFX0ngbu#L^>4N^@ zzl|=sPwY+7Wh->q4qbLamp#zs*MBFv?1L@`po<5(9D^MjnT;!ju^TN+xL1xxi_5{4%lN>8De~$lWW7*2gPVgJk!0(Lz);Hr6 zUyAO-`CprlR(_M!V9MVkTJs#V>pK2m`VIWS^c(mCX>Z+{Kght3b1;6KdxP=S43)f4 zWStk=b}xQxQXZus)13}Kq`=P>E*o;iQWkV&I=qmAOt;W9{EEdKdd0s!`{&#d$TQ;% z|IgLLgo*oVFg}?7wfFX#<8&X)8{hp+{NB7tL(w%2bDbYVj97U43*<1CI;cA4kPCx{ zxRTk+jpUdE53U%zy9K^om9hQc!&k!7OPhZi{JGpKjDq*f?UyQ=BKO`I4=>+34xrS1 zAt%wi`u;9Vt6El?c@|!`86MXQPwMX%KkqL82Y7d>yVA=7JYB1@@6xAc5j#dvVr<{! z#Q#-!tOb2UGk(3vYO+naxHr>^&c}u>C=4A@IJzS{I;9A7O_AuJqT-wx(H)!_YFlT9 zDauLOX=g@|#hDQt1PuR4=n)Zu59R*SVXF^(J-%VT5^CEYKg@<4&!(w%8~Y=h-4t!> zrI>6BlptG*YO~Ez!)z>Uqt<)rK4)+WsO3n>90pzv+u0;_IK2qYt#!or(4{5 z-fu%h;J!taD{(JHD8Ti2d`rjH)@O~a-FkII!$OYZ94dOSdR2AQtI>`k&P8gJ<7ruD zEp3yoMCYynz6RL4*3yZ+m8jf0{@1CIj!1N67p=EUy!23~i3v)i&4iu|UpYsPa_dB5 zPdjoR?J^O0u)~b5tVK0DPDhlsLFStqWeSr2C(8fn{6|h_OQ7#f=+9*RYJ2I#9K{SC z_h{q)uipnBS&+>NH-^Yw@P2#Y(cFbzqg?0ovW(6s<DbCRic}(Vk%98=k&bbE3-TJ7hyUF>C=-cu&wV1fPo#qw zSrdx52XVP35c}S-hX2I5o1if!HXHwQIp+eyug4+B3gfs9GVTc$&T=g4ZN z@#~jU%UtIc_TN_a;WqZ;VeHGXZ+P79Y8<5a;uYs($$V={^uq09_&{xU>ePm@T))G$C3&OpBA1eS<~a#AN9sc7u1Dq#Z*Ujy6lr|RiB-;B zk4_)b;O@CgaGu|7&P3-8} zu{LA8Gvkhlo5k+zj1PPrsSkkjZ#ll2;}26XP6r zFy>y*?&K)qx%u3uD4|_#KKCfflIN^P@1_28F$=ekq<$tiBdEJF(c-w5dX}s>hi(^P z8dCoCV5jfJDaiNoi=HVoSv)dE@j>7Xz`hn@Spb*J#gZxNrG!)sT@Zx@B}FN@-l%J%f!Ek;52 zx}G&6g>i5$$>}Siud=C|{q^vM^S!IYdB(+&;BBJE?YqSJ-dn|y-W$Z+-aEybK0NPs z%EKt1Bg?l^{;F8hdl%(fDBmf1q)VKa#oTo8ZdH8Yk8Ts{{H@~6;8#Rb&}(98&`vQU zXqQ+T`m%T_WV=vgD474b>m-QsA!4Psv3t>UQxyTsAHE>YKir|8mm zy{Id$5%&JO#Zx+7t!V1KU5s8V+p(K=)QG33Gq3M!)Y~F@_S-4GLho7Ef0r23ce|L^ z?`81`b}UD3m+wNG`)sAHyJ+hcvA_3vv8JSk>)l+}iX*qLr~R*rF^lE1xo`jV)ZZ;i z>4&;}r>4{tJyWSFo*s*9#OU6;!L22)%6c(ftr0803CpF&t*(IIggy;r)duKs^$8#JaREG&Ua$_Q%jrdlewN(^vr9> z$cuoNUd?Y<-SX5kwSW5ce}rc`#B2^Mwul(RrGj$*~HOG)sJct z!o|U^HvCT`JdxE;Ol$#mVeT&{_EM{Uv<-j#DCbu3Zg!aX2>ulPo3@GPzC%BL3@x9| z4i^`*Z6d@J;)$Aaua*ET)TDZdGdnR$udj-z$&<8qvm?c)_`XJ&Vzfo{-3L=9X^VK@ zr?VrdA0|4PVmudzsUkgE6}__}MK^GwCNI~PbH6^OAkXm!R8c-GQd}AqA(nSh#pyQS zL`R79p=cON3a_B}|;@5>8Bl4q7bq-k&!``-0~t z-C_e4>)DBpFZSLo;>3O7;@N37@htDRk^5QlhiV(S-xH=_&wE^N%zIXQmU{b@ww?jA z!^KEouTCbu9yF~(KWOi}SzMvbjd?%Q&`)V5XzqeNr1{qZYY*X8@KtGK?zb+%Fdk-YBg^rhcd^h^+A z^wi`s&1|=c2bCa?{2opx@=fFo(H@X^1Hc=q3i+(l%x~SIBSlb@DqhGd)8x02$hQWR(|qfR(={J8Dz z4j1`(HnGF#!*u%aGHqTy`FZU<==d3Qyi6ND;QwQslP3RMOPAl5DcZA}>k6*>a=pXU z*7Gd&e*UNYiUIdWO5K>}wvG|v<<d=@2PCpzKTLgVzgHamVa1@%rpY(Wx*(Oq=pE ztpoLM>l`8VWuB>e-#@Rl2X;qMn3!yC@A;B%-ZEPiUot;TqyC8aNU^>!Qq;sph_2Rf zk(^)?E^CCSNeC0xZehX_A1->*ua^9$v^x4dB`KV5-|k7t|Cwe79yQqyPfnEIlHJoa zQWd8rFVo(Ew)-bPq4m#yQj5)hUQ;H&prubP)h^P1uE|ep^Cqv-RzioB3bGgO9dtFn zB73~^nWYw0%;dbzLc2JxwM2-wI4`q=3C~1wH1!A<=Q*$LZ4*7d9?@`$z1Z9!CF6Yl zQf-E!WTX|3Z&)|GbDsqkC1V}y<$`c|9@VfQO3A3tU!$#=5Ho3wHNr8st&(x%>ad2n z9h8jGd26-$2@#X(ZwYtIk5e+%Ud?KlA1}`%8|HUYGK%zi(UaChhdJgZD;aaI4r!R% zL!L)B%ih^y9qP|MTDAwAdfl5Xg?Jmk+t>sRLoRr%!%rPfZ$(VCB zyJ3z~o^u=K3{f&1yo>W`51hc*C33MfWNhsl0d5ba@`tkiz>G&Ej6Nj@9OvU8rP?MrBJsyNAXSI*(2eF)4`i6pABBF@$6il-5b2Sd3K6~8_&+=*?ocM^K2K-&NZH$ z%d-dY>@hrhB+u^2J@4V!vt?c5*||JBmAZ3y?rfgx0Ou_H`D~uuoEGFt;oXb$=Z86p zcz#k=m}3FYZ!p zQ|>6{`II}(!WV?Y+XYX}ZU}=nd=%b);LMF8Zd!K3(*;|^8Q>OEsOBB6_xW~G|+qvHkvF@%a@dY?7+~=&>UOVnicnaz@!>^3t zy_$2JzPsRE7c8!$9dC$ZcggzOsb3>5Vvn}L%c6z*JP!VX!JEanJ2#4Ev#r+4 zy3mQb;|6aLUe*;G|Glb>JRyqj@M5=cI5rC}Yf2<^9H&NVUgn+Tu3LnSIj|)ke&p_= zXDsk1Ui!BNIwW_kf=}8dRwiy1bD&=?dnL*>$)^{>^CfqEnK>6epYpTJ<+t)=IVBYmIPf8hJ;o|MoWsi`^JtTf2_vyNsdhFk*dzN|KVy7N`?uGZT z&|VufbvWvTEp@v%oV;7CPJIpDtqNXlgE;HhC=RFW5ssA2f_;R(r;z)GQ0B0MCGtHs z3BHZp*<8zg$-%C>d1jqB7{7yOZs(aBc#l1-8}fbEi#dxo@f@B7Z+MRQg8765dVZ4qCEF6mp#${|3)q z558L*E}^~2o5Y+H_)MN9`Q-`uqqWu0DS|#IEZM}mRwr^3sZ%2BY!W$gt;)~Q3gE*> zsAiAXtoU+@H;K8>`9*l`VkOSw1;@)bWR~=nIPluu;lx9daCl@Z^^^JU@2?S>{O;r) zjyY5B8w<}pl(<`@asHrWhxpmdP2$STI`P5GJ!0T=xA?Jd|YHt|kpy1XGCyk(C_ zimMaD_yxwy?Cv|n+S}?x-CcDe1zw?r+%;G7 z{k)j83uy^;`i|YTLzMS_L)a-_F@<<)@W@?Ec8_f1bMVMd!6Vnf2iH1!%D(ojzRNAn z4|9uicgVikE<8Q&2)ZRqSy#h*4=9 z#OOXRi}|#9UH(|@2+wfRw-xZ(F+9TopPdU&?e5h_oa+mi7_+PP)P0zA)L(-*WBw2%DaFVxvTGY=r3EE=F{ZwTZ8| z|Hiy~w9l9eHY#nkXSu!%|Nr~Rc{(5bI{aIAVuy97UX`L~-Qfe9m~#?%*0bzso(DfX z?|E&F8tmCiTNHDYh*u*-yg5cF#G?Bld{}2C*wctTSe0qL*lUgeCkB4XCQ_2@oa3}o zc(x*APnp90rxDwxD$^Elv9mGTxlf4p4$oVj=YJQ4`~8qUQ1YJ8dG{io{SooUHny)7 z9|O1K57YM3&aKSTck6qvpKI8Id7j`NXY!xWwoNjYx4bVC}6C_a{U?a)QWP zJIMDPQvMZdz*lok;;VTvLd+%3)Z7U1_>@vzwh+yDPB$r@={zTW@{3v=?d!ZCM#h8k ze9e6S`-Rx2O^p(l7ZR_nLxebUw@tKkw2LR>BE-7P7_p&44C|*&$UbkOKjj`TD8`13 zW3Z<*uT(qDdvu@T-*-k1wu=@u2%h`}P12oQM4q4|lNd&M(&Q(#ho`L26vp@$`K8(` zc`s-S;OAH6uhTZe&xb)9$fq0BiSy-z|%Lff3w5W+u288qn0rqo_-s=Z4^BHHh9}8c=~Pdwo&l(3-j22 zz~k0gqa1d4di$S-HQ3?lpUQh$i<%fQDXNRz5d}}b4W2#`ez_z}snlfP5t`kkiUjnwC!;c?Q{!${>!1e3I;PR8z*rEjTPE~_X z*|qX&Yxc_+;uFYHr@a6L*z7bzPQMNaf_8B%KiP&$^ z)|0^f_0gB|>}%!K#2C=)t42BcSN;9`dO!cWOTSEiJlyxtN67!uuQg!L^ei!JXAt{W zwkc4iKkEj(Kv|&PKl)mZlfW}&`%WhM-|L!>?mxJ_vQJyLC-DD&ias|!|9_f3H@4?* zy_RD>Q0JdvJf2MS`>Xv=jkf?TzHfY;WUdL|{3GZk=hT1X^8(NMzW4uom&-Xi@Qi;1 zeFM+>&%Uo*g97*XXKb%r!~U7u7ie36{{L+A=}CBlKpX!V`j_~**R`jn><51q1N=?$ zjis|316S5(1Kjlid-}qxo8+N(b>o@ZH7G7hCH+fvePK! zp;qLsHbWk3Lmrv{+=?tT9a*B3q1p_YtPOc6vfkQW@XDdc6s^b?leo7P*=SefU^=cL zo3$Yum2%-kVr&E$6G^r0{Il4_RF5zc607tCEHP@!)t9DoEiOi zR&XBgu+5p#o#({%aPDrFID3t{Z~LE-A)gVIv+EA|jK0p@0|U=!H1JOQ@%keJT!s9^ z+Dd*h4;jZ8WE}SlsuK~&PA(ujX+ck!rprC*#GnORkR5Fip|{qF%v-mLB=(YzPZ_SA z$wS{~3iXt;XUq=UDokNpMAxts@s_PlWJ_J|w4!HTG)H^NkqN!R^Cpc;7deqBVhH+V zbL3W$7+EWVVp2r6m~ln2E#>Lry@|4Xt7sUT!ZYf`ZzHw{a}4;MQ^Y{pxi4a? z_$j)?eQj#REZ*bcyyaRMd(~O!=gPR=j*d_2>z?KMW%jbC*pEut#zrN?^8odqn}V!k zL(wyjm_t1GaQ$SS&}OkOA7T#j{Eqt1POj9Zaeczn$@5dLH|F`T7jZ51W{=H^rX3wT z9g){&An%TvIiw+hcb^et7Z0+Re&pUr@sm6&xlDJVj0ii;FG@3o2@==>ggz%CY| z*PRg(DIV-%6_4C!6N@HCi#7E9n%nK-xd$S|)98bjhFHtwWJVjH-)Z!eQl_+t z{}sRs@+x#abp`tAJmAX>J#Zd!jP@#eb>tYskTYPj=ShOjI}APalv$DD3}bF5_xOl= zpP}vfw09G*pHMGq@^jh_V55)+7OCw#MaUTPk-3a^M&r{uq&z|H*(t}a%(Fl5Ic;O! zlZ@jhv`6w+Xg>qDD`R5@GJ%D8OEnE0;+i~7+Xub{*^LF+gPFbW2KMLkqU_>r~7KG753lJ$Qql^GH5_Fg zu_J3Z%AVVftl^VVbNYUj9Z71ahTOBrJ4Gv@tHe?Oxm~{PK3i`v8j?8(; z8b%^(up(<1iLAkjtRV#)fBiO-a_!Vd*5E+aU_;hm<6WhGFa=qI6G0$?-XPWR%8vcd2cJShS|{Dfv&S;GiJ*6`OI++hmqbls1}Y&7~8iy0+CYi$Qri(|7Io-6I=QWDnKu$enfUYl3mByQBLw*XpPucRcC7o$g(s|R^8dgA*$mZJgR^Ktk+M{zvFF$3Ss>G%{c<#>SqU5(rr zW#m9uhkZ!U68y!NgbA5P0v}`iuOr>#$S1KLfA4DSIhNyZ{-vOk8pVzw~gCORkLx;LQZD0KB*HAAbk`@dA8fC0;jj zb0mY)mm&mx8QwSWC?MXx^{ zu9)!kn-xV@4_3sJL!ia?qlQ)ZkEaAjxzDTi%AWM2^g;K-Z0@t%rw1^ZJ9?y!r@9a0 ztKJLPI0Kd(Y;iZMVc+_2sv+0;a7w?*=0WXU-aY8zh*#!KYv*drK~}^ed7F2up3-$1 z-lTk&4f5?hC>nO zr+DWzoX28&UbncHs6h`@htK6F_*|;0UGoyBLBVb(8anmir#7Ayk zDd{i!-_ji#l(lhvg>62vQP(9c#&yLUU3+S{;*#;tPi<0sUz^RftH5`2{FX!d=*WB+ zi|EGzoIk%m$~BGir^KfCvI76z0_3RG$PUeZ*`asS7#I0Uv=riIpWQkZpW_ai%!7Hp z#OYguUg<5y=t+3TI&y_10`G$DvpcPSdF657 z@g;AG1aw+{xayAVTUxmncouL88%LQ=*SLw_!5Ub3h7s=v~|+ z$;A=f3a0zQGw!i3*Yg66kaH2aKx_t%B|1)6`EMadikzRkjJqDtb{@z4#huAl)LHZ< zS4sSmIFYqDP6XDJ9Omq?%-P+R924D_M2bVI)jeNdQ-blG4`K~C!Wt0HT5(K_W9{j_ zWH0wnL|fpYzytY}@|oAhGp`L`4ar~)d7b`xgEe3n@iDSk$0mcH3x1C!N12Q4BAW7n zwDS;i!hA*H+p`dVA((g!#F)&^*_5gL6vrfu$A*5Q+k(Nm1+w;8iX8MAe2U|m1L zx*pE8e_fwR9~IC?e?F$`S=S?&&;4V18F*!__=s=U^?KHI_CChCz7M>5@YWgY+(P2Y z6k&5J$MjHWIfC)Zy5qLdZ*qNe8ZiHu_R}*Rm>k#sF&(Xs>39RSN)2jOi%G?JCA%xG6#((@#^r3cMpG)m>%k2u=sabgUmoVNK>ar3Q|F zOv`*ya!iN)`Ivs}`Y|oX@{RLDH1mV;GY^Fuhav@<_ML2H?7=^*bf*IzkDw5H`j#)_ODp_F!q{BOG1K^duB~B= zuVBU(Jc)Y&b1pU?#9Il1kFRG9!&aPf$;Su7KPm81w*gNAzMXlXj(Ol&Xj(353cNk= zDa;d7nJ0b@P37L%BMrWY`)VdB?>wQRfd_u+-s;4!TP@tJV`?jxDkx$ zI_UTibgW}crZXnpw9mpgcbJsQdEia}Hy7Mgp0^izUdG4ZCC2CHW~mSD;3i*VC2?=u zDYOM2vq~p*5}>Q2{xGqlHpT{-ay7(QNNvgXJ5aLH77Cmrjnyw&b?{)OZFiS z!~o7^=xCXLPbT{L3VD5!xj2jcr{puLLUQV=p2XiaG0W!-^W&e~7h0Z0TvVMek@&5y zb$l7WU*dl%8E45PhX;_){Vga0fs_|M&% zay)fbeV1-!JSEEUfqepfF8j*6Vps-t5YIGj z&2m}EOE!l#w3x$cn>42@^@x8zHX3*l@Kf0D%{zjf8~?Y^_Bzhv*^|{T?kx_hHqSQp zWEa_!$+%B4?k#+c$D@85_x2L73ucQaSB+SWpWyKp)j|xs$XeFK)se)yV{c!}erI(W z|M_lekHP;-AD3flq{Q>2snOba?j04PlwTY~JeI*K`-F(q>|#5vO`@xYEx{ z-A3XeH4*=)>PcH&Gy8)kb55PQbXlgdv@G*1c3{ms_v}{MGll>AWS<=b|4^p#gsJXx zS)aBv8N65{XW6!y_^#{UCqC{oIA-G8u3vMW!4W9`Z>ww2Byiu-8=hNw!*fedVk;us z37%}1Y=?}^qyLxh!2JSyCrM)&$0u;zn#PTZ-@dyyab(6JXr6qFC0EkB<~!nVOMmfi zaw7k#uefZ7?yJhPuRcz0SF`EQWO2y~_Wt2}?^G=DV-+)}uUk7U(rTi)dJ&tQ%mw_qa zjHNzmM#@UA8@NXA)I5OeAMaog! z1wXh_gp6?Wz8_p!gN(4Q&wW?wkP&VvyYEUpGQt*Y$6LU=Sdeoi>O@Y%rF}V9(oYU+ zXzDZMO2Uz>hLhNqW?vcIV3z*eJg1=8G--C+$lL;W{FIbxaTq?Jxw{SDSNvOUw~3y8 zZ6X)@xFUE3$#wg`CgD-KV%;M9D5qE3yM_RC>bwc zjq1!;v)D9gaMK#skJF`5Ry`3lE$U{X#zV{wRB#*qkL)(zh43HZLX@OYu{oo(5-EjI5S6O4Q^!~c-i2cBp#-`UICtzWB(Zb zRmywv;W;P6U;c)7o`0mC{dt7D7xE{S>wSC=lDD@Y6In~!l~%A3z}5qk{Jo50D|5?| z%h$aO83Dfh$ZtaRyuNAVH@(C?BrlPCUx?@=&*bx!@)d>iL1N*WiS5_~PaxxY$UNoU zxgN=Pl2x1{60q}iv5q&hjwdKqEg?uLZ)V-@O77A`bXMI|3pUolo($xuQ#poX7ye_8 zRUC!%%ZtSpv4`U>WUT#=qt*~}a}zN)pC>lv8RC^bPwrh}KURJWy*@_1YEipXCLxb| zjq+cV1c?*o_U^lo%e@NTizRQ1XU%OZGfVano3smXGk%}+#nJs0`-x5ZF+K*9v5A*C zV38wM_FWuId3*O{;$5aJeg_#q`^x_E9(vs%X{%ltN8KmM2m3Mp-^Ilp^t$k0m9tBN zWx2b!_yh5H$vb-9+Dy(FI-LBrz<&n*!^GO`%sr4ycqPPmC1&Z5q4@{Il6{~2y2Ofg|AySU zCy3b^TiitqMD{E3I~4yx3_|`(tkKGM`F_6Q_=23v7uj=lAouT}FUeQFxQhrW?u_0f z#Jzv4U-s1qgd|Tw;GT-=ni$CPu_sV{%yv#G@n7GXIeskpg3JJs4*S(*5 zPqG*LEY#_Hw*I#j(x2f3v0!B!&u6E_is#8!yZ^|6iv3e##bws6*LL=H)ox99Z7yqb zr70}hbtm#A~&f*&&*9?uixkvLfxu-qXC){-o-w-*^>oF@ipG~s6Qn+VxQH1M~ zIns4uaFpxS9P@E2_2f6QgLBH!VcH44lUUXeDa*Tj%IflQ{l#x=F5mr%Pi5XqP~i3U z$+&!eTU&3^aANO_aGf8;oM(J*GT(&0&KNq5z}gtlaX57yN4(flKSgWdxxJ5#)#`a( zG<(V@uHj{6>{L&3*LX1*+<68+D|uGQ&${5bi7|)|x(z-%(!+k%!+PgI7oq2!Sc#rm z=AQ75V&9i2`7zpC;I{=H$o_1XIp&It=Tu7Up3-GYGpnkxUxpUa)=;ixG7hHSKG|=} zC1EnAk`BY(HE=EY&y(C+Uavo8hClY(_{!@-VoA1>b2+gkP2`S^U*0qp9YftrlT!B+ z;%MtJK8<_Tn5PcZ_b0Jgbyn6d2lDxI>>=io|JI(_VXO;!)&Z~M98dhBD&mJ)S>qFo z_(l5M#eXNUiV8T-TDCW{H0~6!xl4)L^k$}ud!$Hyg!s*JPt%QiY~UVCiG6fvaew9n z{P&p;&oUqKU&~tFG@mt1-@mM6|H9l+TS#u(__%8BxinMKqA4fn8jt&^!=ImJ`Z9lh z0vTUQ^1lI_&9>&YqvZR}04`}G-?yErw9#SUNtpbvvR2CfD@_V*ADo-NdDObLgp>#lo;-`Ip?*At{`D?P1>*+qb z7k-@3y>OW7n!}tFn7;ya|MuFSA0%)sztQ#dPwVTz|C5GKNa1Zx@(*RX<}wfdU!Q%y z&g8GlyRPrE=B?p+pLq}1U1uMW>l^xJjzFKiFyHBVaaxvZC3K&CY>IY;^>p>Iky<~# z&qMWon}f6Pg*Ic2ZpPpCZu#9({^cX&ye9|6S$JW&4m1~FS7FBYc%0(97Lz=O_0;HN z>1!?h2%C9+g6gbGVVya-F1s$9wa&qMHHtM_=Ce#qyF=_|eetor6saAx9@v;kIT^9a zG`YUbV11PF&RXDEkb`+zfL}xow~clDTpyb&%oMH37|<kws^-su?&lJnzS6jv^RY#$o56Z7<$xntM`x?n)%{pY z_4Pl|bUd&=)|5>#)_Zule66bJ}Za>$v*(G}h9)SyQL7woYM3o(&D&V*`_^~utHhA< zFOV(VxDJ1gy-Lm6Y;3NEy24be)}wEZYc1n6%{k2V3cmmGK|@?7B}ywJ?sqrEteqPa z?Yb1!#&sG#Su=Rk&4!%DZpe6h9f2=`-pBa{BoDfaae77PgF4_-ZqdhYa@RY=NO(-2 z{#|s`&cbKrLbEV@xaD}Bt3-Ibn^f0I{-51ya!FtHM))JiBb_WT59K4T^L41r1LgwF)vWB7HWrK&Uh9P^XM~4-GeBv0o)<}^CzSI{D zg{C3!^)_@x*qFF`6YGBhFx7xbJBTnvtsD<*I(;DZak;!x5xE4b^>QW^Q4nKMrs^6^H#izaf}2d)G2O5;9sVBAl?*Qp>QMtxvT!mbUNEc2EP&5-+r zQW>{Csr1gKkkUJ4zOB_;a$IYOSN{p~(uH$QVwUf{a|Zd_*E1)?$E}|)aT`Mn0k^HnRlRlsR|AF^yH1EAr?vHG&8KYRgdeF~x^?tc)eZBuXUT5qjFEVbVyfR=$PibrH2ZZfJ$c4jhde_$PHCf5JCK2uzu>|<`BJ(6Evc+|hgE+1~ekq~nJWZ&o}_(1{#K*XYy?orpoI(+QneZF6dSm(`>~GdVtp|HFTc|Hx1& zi*;EQRqChozV_}zzD#>1zbM=PUF(X>Y2o)*8Opjcgmoo{btRj1C5v@szn&LL)|GmU z8}pETV~)`CL|(sk+;T6HK4yD9hP za#*9!Ye9$CIF1|)lWsl-!wA-@TzD-@SBD7f<&jBiE@UMEe(nhC)*4_v>E}Amr>CI% zyGQknP{NS6!0+9UKCnRbeexjiB93*$fF`yvv3B&_;Y~6(_aT319|SHAGkm6{O5_xg&a+k1?I>q_W!MOSyZ7bYn{uY3Yme9Z^#Lx zZsOaq^F6*!=7?*)PJujPf${Pebrk!oy8nD5>-Y8J=ezB^kLxl3d?Z+BrQBP}?588I zU&;}%$NN7S!1u`juGizckZtNRfCIO?YRh7-sD`Xj#s&z)0oYSE>57*%&@uyg{U4wI z_mRKf{U3)y-u@34y01lMt9F)k+4;oqI&w4Wd^GWT%fH&ndiG1!d8azxx%7!l^$BH6 zV7xYRufUor`&(7_-q{pZdZ*0gw3j?Sjh##He5q_u-D~EKS7f){{fVdeWEmBe0eX;C~8h$!gY) zff8=4BX__v^h1{YwK>M)J-{4=j-+bE<8?LIXD(+wJq@pNGBUd^D~`PjN7af!b+gTJ zs~3eR$1Q4%!Efn2%{P`35IhnmiAl|&R^PdMFFpj?5oSr7O?EH!AyJFCf;2yI%0`?`Vu>o=O z{FzJl>U_c}+9z{L9b`Pnykc@*NB5&mUs{@33;$HJsh?|Q{oPu#bhQ#Bo4 zVh+zY!B=$Ue>S{_?* z4i7D9z&0K~8}=Nt*>jwvUJCj3PO@hrRylhG#VzgDs<1_z!QRA#E;9?8*!9>YE?~bB zg?>}|8*E#0P~ro(;BTN*#@8RJh({Otl3|}GZ3&T0R32x~R8L#TG2q@pUG`L!OM$Hc zrso4h-s{GnhghWODfd=X^E_#T*GL~mOF0MqA)nWq=SX|e@$A)d0eC(+4qp;|H4i>2|LnkHe7|D}f>A{~u{+BwJF!VKYKf8uQ+M}X-)$^qJ z{T+gIKa^U7M?DIUx)q$3#eId&8&L;)C3wA9t%bSsUE(>l?6bPY0hhX@W2Q)t#}wvi z*=lo1y8-$C9C{)45aoKl?>-jS1#~WYU++`PWsKdxTCM903M7B;-#f{i6ixgO=uf21 zK(7am91q{m7h3;2^3PVQx7URJrE`P<*GRo(A-KPl%@$=wV8smr&LnVT)V zpKRygAmyQ=a3$~ZlsDkxRaZn`nL94ab&zj*Hs5VH?LLOwV;jdSjO_xBAsj>bhM(j( zfb5|jnL>qO7yD!Uetv@A&ldb*R-hv%2ZH;O5`+HssQ6U3pWTNYxEY_07`=Qu_#45O z_V6Ep_aNW;BEJ7ujqiCLIYZIcR#srouw1vh55is|rgAuVlfY}scifKec#+ieg7+SH z`@wqv{jpxpkadhhCi5QOL$CV!it)&19Ly8*vB{`aY`Tw54&RuRv*hA?!@BK$3VBVA z{HB;Mq&}_|na5FN9v75|%39!W13SvMFKv1+kdKx9CwW|BINRM%A^VsMjvIU3q6t

    4=~7Wsi&_`)~6;x9+Wf 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≧ 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 M+k87u=N47}0wVYh#{d8T literal 0 HcmV?d00001 From 759b0c8b964740f65c3df551e81686a4decec5fa Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 15:31:33 +0100 Subject: [PATCH 019/172] Foundations(iOS Sourceset): Use alternative implementation for MacInstance, due to complications with mac length under previous implementation, which makes NIP49 tests(with all other relevant tests) pass. Some spotless fixes. --- quartz/build.gradle.kts | 4 +- .../quartz/utils/LibSodiumInstance.ios.kt | 67 +++++----- .../quartz/utils/mac/MacInstance.kt | 42 ++----- .../quartz/nip49PrivKeyEnc/NIP49Test.kt | 119 ++++++++++++++++++ 4 files changed, 169 insertions(+), 63 deletions(-) create mode 100644 quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 3a5305c33b..9f6cf2b360 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -292,8 +292,8 @@ kotlin { implementation(libs.charlietap.cachemap) implementation(libs.net.thauvin.erik.urlencoder.lib) implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal) -// implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4") -// implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4") + implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4") + implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4") } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt index a14715e889..3b56e181b8 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/LibSodiumInstance.ios.kt @@ -36,17 +36,18 @@ actual object LibSodiumInstance { nPub: ByteArray, k: ByteArray, ): Boolean { - val returnCode = Clibsodium.crypto_aead_xchacha20poly1305_ietf_decrypt( - m = message.uRefTo(0), - mlen_p = ulongArrayOf(message.size.toULong()).refTo(0), - nsec = nSec.uRefTo(0), - c = ciphertext.uRefTo(0), - clen = ciphertext.size.toULong(), - ad = ad.uRefTo(0), - adlen = ad.size.toULong(), - npub = nPub.uRefTo(0), - k = k.uRefTo(0) - ) + val returnCode = + Clibsodium.crypto_aead_xchacha20poly1305_ietf_decrypt( + m = message.uRefTo(0), + mlen_p = ulongArrayOf(message.size.toULong()).refTo(0), + nsec = nSec.uRefTo(0), + c = ciphertext.uRefTo(0), + clen = ciphertext.size.toULong(), + ad = ad.uRefTo(0), + adlen = ad.size.toULong(), + npub = nPub.uRefTo(0), + k = k.uRefTo(0), + ) return returnCode == 0 } @@ -59,17 +60,18 @@ actual object LibSodiumInstance { nPub: ByteArray, k: ByteArray, ): Boolean { - val retCode = Clibsodium.crypto_aead_xchacha20poly1305_ietf_encrypt( - c = ciphertext.uRefTo(0), - clen_p = ulongArrayOf(ciphertext.size.toULong()).refTo(0), - m = message.uRefTo(0), - mlen = message.size.toULong(), - ad = ad.uRefTo(0), - adlen = ad.size.toULong(), - nsec = nSec.uRefTo(0), - npub = nPub.uRefTo(0), - k = k.uRefTo(0) - ) + val retCode = + Clibsodium.crypto_aead_xchacha20poly1305_ietf_encrypt( + c = ciphertext.uRefTo(0), + clen_p = ulongArrayOf(ciphertext.size.toULong()).refTo(0), + m = message.uRefTo(0), + mlen = message.size.toULong(), + ad = ad.uRefTo(0), + adlen = ad.size.toULong(), + nsec = nSec.uRefTo(0), + npub = nPub.uRefTo(0), + k = k.uRefTo(0), + ) return retCode == 0 } @@ -86,7 +88,7 @@ actual object LibSodiumInstance { m = message.uRefTo(0), mlen = message.size.toULong(), n = nonce?.uRefTo(0), - k = key?.uRefTo(0) + k = key?.uRefTo(0), ) return ciphertext } @@ -107,17 +109,18 @@ actual object LibSodiumInstance { out = k2.uRefTo(0), nonce.uRefTo(0), k = key.uRefTo(0), - c = null + c = null, ) - val resultCode = Clibsodium.crypto_stream_chacha20_xor_ic( - c = cipher.uRefTo(0), - m = messageBytes.uRefTo(0), - mlen = messageBytes.size.toULong(), - n = nonceChaCha.uRefTo(0), - ic = 0L.toULong(), - k = k2.uRefTo(0) - ) + val resultCode = + Clibsodium.crypto_stream_chacha20_xor_ic( + c = cipher.uRefTo(0), + m = messageBytes.uRefTo(0), + mlen = messageBytes.size.toULong(), + n = nonceChaCha.uRefTo(0), + ic = 0L.toULong(), + k = k2.uRefTo(0), + ) return if (resultCode == 0) cipher else throw IllegalStateException("Could not decrypt message") } } diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt index 52b65b39ef..9b18fb5792 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt @@ -20,62 +20,46 @@ */ package com.vitorpamplona.quartz.utils.mac -import dev.whyoleg.cryptography.CryptographyProvider -import dev.whyoleg.cryptography.algorithms.HMAC -import dev.whyoleg.cryptography.algorithms.SHA256 -import dev.whyoleg.cryptography.algorithms.SHA512 -import dev.whyoleg.cryptography.providers.apple.Apple +import io.github.andreypfau.kotlinx.crypto.HMac +import io.github.andreypfau.kotlinx.crypto.Sha256 +import io.github.andreypfau.kotlinx.crypto.Sha512 actual class MacInstance actual constructor( algorithm: String, key: ByteArray, ) { - private val cryptoProvider = CryptographyProvider.Apple - - private var internalMacInstance: HMAC.Key = - cryptoProvider - .get(HMAC) - .keyDecoder(digestForAlgorithm(algorithm)) - .decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key) - - private var hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction() + private var nativeHmac = HMac(altDigestForAlgorithm(algorithm), key) actual fun init( key: ByteArray, algorithm: String, ) { - internalMacInstance = - cryptoProvider - .get(HMAC) - .keyDecoder(digestForAlgorithm(algorithm)) - .decodeFromByteArrayBlocking(HMAC.Key.Format.RAW, key) - - hmacSignFunction = internalMacInstance.signatureGenerator().createSignFunction() + nativeHmac = HMac(altDigestForAlgorithm(algorithm), key) } - actual fun getMacLength(): Int = hmacSignFunction.signIntoByteArray(internalMacInstance.encodeToByteArrayBlocking(HMAC.Key.Format.RAW)) + actual fun getMacLength(): Int = nativeHmac.macSize actual fun update(array: ByteArray) { - hmacSignFunction.update(array) + nativeHmac.update(array) } actual fun update(byte: Byte) { - hmacSignFunction.update(byteArrayOf(byte)) + nativeHmac.update(byte) } - actual fun doFinal(): ByteArray = hmacSignFunction.signToByteArray() + actual fun doFinal(): ByteArray = nativeHmac.digest() actual fun doFinal( output: ByteArray, offset: Int, ) { - hmacSignFunction.signIntoByteArray(output, offset) + nativeHmac.digest(output, offset) } - private fun digestForAlgorithm(algorithm: String) = + private fun altDigestForAlgorithm(algorithm: String) = when (algorithm) { - "HmacSHA256" -> SHA256 - "HmacSHA512" -> SHA512 + "HmacSHA256" -> Sha256() + "HmacSHA512" -> Sha512() else -> error("Algorithm is not yet supported.") } } diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt new file mode 100644 index 0000000000..c011e785ee --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip49PrivKeyEnc + +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.fail + +public class NIP49Test { + companion object { + val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p" + + val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683" + val TEST_CASE_PASSWORD = "nostr" + + val MAIN_TEST_CASES = + listOf( + Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00), + Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01), + Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02), + Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 7, 0x00), + Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 8, 0x01), + Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x02), + Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 4, 0x00), + Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 5, 0x01), + Nip49TestCase("", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 1, 0x00), + Nip49TestCase("", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 9, 0x01), + ) + } + + val nip49 = Nip49() + + @Test + fun decodeBech32() { + val data = + Nip49.EncryptedInfo.decodePayload( + TEST_CASE, + )!! + + assertEquals(2.toByte(), data.version) + assertEquals(16.toByte(), data.logn) + assertEquals("52d7c3f8580e7b41953381e5bc49646b", data.salt.toHexKey()) + assertEquals("c33f02a7dcaac8bdd8da23cd449783240b6ebc12edeea7bf", data.nonce.toHexKey()) + assertEquals(0.toByte(), data.keySecurity) + assertEquals("b8e8803440de7b3e9519c3e734cb2ac9a211ea2dc52312e5117a11a3022d813ab438719ca0b504a1193be510c3aee776", data.encryptedKey.toHexKey()) + } + + @Test + fun decrypt() { + val decrypted = nip49.decrypt(TEST_CASE, TEST_CASE_PASSWORD) + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + + @Test + fun encryptDecryptTestCase() { + val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, TEST_CASE_PASSWORD, 16, 0) + val decrypted = nip49.decrypt(encrypted!!, TEST_CASE_PASSWORD) + + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + + @Test + fun encryptDecrypt() { + MAIN_TEST_CASES.forEach { + val encrypted = nip49.encrypt(it.secretKey, it.password, it.logn, it.ksb) + + assertNotNull(encrypted) + + val decrypted = nip49.decrypt(encrypted!!, it.password) + + assertEquals(it.secretKey, decrypted) + } + } + + @Test + fun normalization() { + val samePassword1 = byteArrayOf(0xE2.toByte(), 0x84.toByte(), 0xAB.toByte(), 0xE2.toByte(), 0x84.toByte(), 0xA6.toByte(), 0xE1.toByte(), 0xBA.toByte(), 0x9B.toByte(), 0xCC.toByte(), 0xA3.toByte()).decodeToString() + val samePassword2 = byteArrayOf(0xC3.toByte(), 0x85.toByte(), 0xCE.toByte(), 0xA9.toByte(), 0xE1.toByte(), 0xB9.toByte(), 0xA9.toByte()).decodeToString() + + if (samePassword1.encodeToByteArray().contentEquals(samePassword2.encodeToByteArray())) { + fail("Passwords should have a different byte representation") + } + + val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, samePassword1, 8, 0) + + assertNotNull(encrypted) + + val decrypted = nip49.decrypt(encrypted!!, samePassword2) + + assertEquals(TEST_CASE_EXPECTED, decrypted) + } + + class Nip49TestCase( + val password: String, + val secretKey: String, + val logn: Int, + val ksb: Byte, + ) +} From 2b61f4a3d6b30daed490f8702ddea8bfba32117b Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 15:33:07 +0100 Subject: [PATCH 020/172] Function rename for digest provider. --- .../com/vitorpamplona/quartz/utils/mac/MacInstance.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt index 9b18fb5792..284b5d3cb6 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/mac/MacInstance.kt @@ -28,13 +28,13 @@ actual class MacInstance actual constructor( algorithm: String, key: ByteArray, ) { - private var nativeHmac = HMac(altDigestForAlgorithm(algorithm), key) + private var nativeHmac = HMac(digestForAlgorithm(algorithm), key) actual fun init( key: ByteArray, algorithm: String, ) { - nativeHmac = HMac(altDigestForAlgorithm(algorithm), key) + nativeHmac = HMac(digestForAlgorithm(algorithm), key) } actual fun getMacLength(): Int = nativeHmac.macSize @@ -56,7 +56,7 @@ actual class MacInstance actual constructor( nativeHmac.digest(output, offset) } - private fun altDigestForAlgorithm(algorithm: String) = + private fun digestForAlgorithm(algorithm: String) = when (algorithm) { "HmacSHA256" -> Sha256() "HmacSHA512" -> Sha512() From 3eb2fcbcb8e392a35bcc6cff8dc7165d264e9d67 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 10:45:54 -0400 Subject: [PATCH 021/172] Solves the sorting contract crash by precaching all values before sorting. --- .../com/vitorpamplona/amethyst/model/LocalCache.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 7874845039..750d10ae9c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2298,12 +2298,17 @@ object LocalCache : ILocalCache, ICacheProvider { } } + val findsFollowing = finds.associateWith { forAccount?.isFollowing(it) == true } + val anyNameStartsWith = finds.associateWith { it.metadataOrNull()?.anyNameStartsWith(dualCase) == true } + val anyAddressStartsWith = finds.associateWith { it.metadataOrNull()?.anyAddressStartsWith(dualCase) == true } + val displayNames = finds.associateWith { it.toBestDisplayName().lowercase() } + return finds.sortedWith( compareBy( - { forAccount?.isFollowing(it) == false }, - { it.metadataOrNull()?.anyNameStartsWith(dualCase) == false }, - { it.metadataOrNull()?.anyAddressStartsWith(dualCase) == false }, - { it.toBestDisplayName().lowercase() }, + { findsFollowing[it] == false }, + { anyNameStartsWith[it] == false }, + { anyAddressStartsWith[it] == false }, + { displayNames[it] }, { it.pubkeyHex }, ), ) From ad17924622f1b26899aa3151ca03eb5b631c3eee Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 10:48:11 -0400 Subject: [PATCH 022/172] Making sure UrlDetector returns a schema even if the parser points to some user:password scheme. --- .../urldetector/detection/UrlDetector.kt | 11 +++++++++++ .../urldetector/detection/UriDetectionTest.kt | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UrlDetector.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UrlDetector.kt index e29bf7773d..f572721a51 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UrlDetector.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UrlDetector.kt @@ -334,6 +334,17 @@ class UrlDetector( } else if (originalLength > 0 || numSlashes > 0 || !CharUtils.isAlpha(curr)) { // if it's not a character a-z or A-Z then assume we aren't matching scheme, but instead // matching username and password. + // Add the slashes to the end of the scheme so it matches what's in the scheme list + val schemeStartIndex = findValidSchemeNoSlashesStartIndex(buffer.toString()) + if (schemeStartIndex >= 0) { + if (schemeStartIndex > 0) { + buffer.deleteRange(0, schemeStartIndex) + } + currentUrlMarker.setIndex(UrlPart.SCHEME, 0) + reader.goBack() + return true + } + reader.goBack() return readUserPass(0) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UriDetectionTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UriDetectionTest.kt index 2ad6bd802c..9e73fd43f6 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UriDetectionTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/utils/urldetector/detection/UriDetectionTest.kt @@ -734,6 +734,25 @@ class UriDetectionTest { runTest("blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.satellite.earth", "blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.satellite.earth") } + @Test + fun testBlossomShema2() { + runTest("blossom:9584b6d64e43747364b10276f4b821e5df09f46477b3b8c60cced3e8c647fbef.jpg?xs=blossom.primal.net", "blossom:9584b6d64e43747364b10276f4b821e5df09f46477b3b8c60cced3e8c647fbef.jpg?xs=blossom.primal.net") + } + + @Test + fun testFullText() { + val text = + """ + Did you know you can embed #Nostr live streams into #Nostr long-form posts? Sounds like an obvious thing, but it's only supported by nostr:npub1048qg5p6kfnpth2l98kq3dffg097tutm4npsz2exygx25ge2k9xqf5x3nf at the moment. + + See how it can be done here: https://lnshort.it/live-stream-embeds/ + + https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg + """.trimIndent() + + runTest(text, "nostr:npub1048qg5p6kfnpth2l98kq3dffg097tutm4npsz2exygx25ge2k9xqf5x3nf", "https://lnshort.it/live-stream-embeds/", "https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg") + } + @Test fun testBasicIPv6() { runTest("I saw this on http://[2001:db8:1f70:0:999:de8:7648:6e8] I think it is really cool", "http://[2001:db8:1f70:0:999:de8:7648:6e8]") From f31e73f18242e4ab8e3868cd640548e84ac948aa Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 12 Mar 2026 14:53:15 +0000 Subject: [PATCH 023/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 32 +++++++++++++++++++ .../src/main/res/values-de-rDE/strings.xml | 32 +++++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 32 +++++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 32 +++++++++++++++++++ .../src/main/res/values-zh-rCN/strings.xml | 3 ++ 5 files changed, 131 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 63ca32e910..bb5983ec04 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -125,6 +125,7 @@ Byty Chyba Chyby + Procento úspěšných připojení k relé Počet chyb připojení v této relaci Domovský kanál Kanál soukromých zpráv @@ -150,6 +151,7 @@ Zájmena LN adresa LN URL (zastaralé) + Uložit do telefonu Uložit do galerie Obrázek uložen do galerie Stahování videa bylo zahájeno… @@ -640,6 +642,7 @@ Připojit se Dnes Upozornění na obsah + Upozornění: %1$s Tento příspěvek obsahuje citlivý obsah, který může být pro některé lidi urážlivý nebo rušivý Vždy skrýt citlivý obsah Vždy zobrazit citlivý obsah @@ -827,6 +830,7 @@ Načítání umístění Žádná lokace oprávnění Přidat varování o citlivém obsahu před zobrazením vašeho obsahu. Toto je ideální pro obsah NSFW (nebezpečné pro práci) nebo obsah, který někteří lidé mohou považovat za urážlivý nebo znepokojující + Důvod (volitelné) Nová funkce Aktivace tohoto režimu vyžaduje od Amethystu odeslání zprávy NIP-17 (GiftWrapped, Zapečetěné přímé a skupinové zprávy). NIP-17 je nový a většina klientů ho zatím neimplementovala. Ujistěte se, že příjemce používá kompatibilního klienta. Aktivovat @@ -1042,6 +1046,7 @@ Krátké Šachy Bezpečnostní filtry + Importovat sledované Nový příspěvek Nové Shorts: obrázky nebo videa Nová poznámka komunity @@ -1471,4 +1476,31 @@ Hlasová zpráva Hlasová odpověď Wiki + Začněte se skvělým feedem tím, že budete sledovat stejné lidi jako někdo, komu důvěřujete. + Importovat seznam sledovaných + Vybrat uživatele ke sledování + Profil, ze kterého importovat + hledání jména, npub1…, alice@example.com + Podporuje npub, NIP-05, hex a Namecoin (.bit / d/ / id/) + Vyhledat seznam sledovaných + Tip + Nalezeno účtů: %1$d + Vybráno: %1$d + Přeloženo přes Namecoin + Nyní sledujete %1$d účtů + Váš feed je připraven. + Přeskočit + Hledat jiný + Sledovat %1$d účtů + Importovat více + Pokračovat + Prozatím přeskočit + Překládám %1$s… + Načítám seznam sledovaných… + Žádní sledovaní nenalezeni + Sleduji %1$d účtů… + "Zadejte profil přítele nebo lídra komunity. Můžete použít jejich npub, NIP-05 adresu nebo Namecoin jméno jako alice@example.bit nebo id/alice pro identity ověřené blockchainem." + Vybrat vše + %1$d%% dostupnost + Nastavení Namecoin diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 6d351d3656..2684746a0b 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -125,6 +125,7 @@ Bytes Fehler Fehler + Prozentsatz erfolgreicher Verbindungen zum Relay Anzahl der Verbindungsfehler in dieser Sitzung Startseite Private Nachrichten @@ -150,6 +151,7 @@ Pronomen LN-Adresse LN-URL (veraltet) + Auf dem Telefon speichern In die Galerie abspeichern Bild in der Gal @@ -645,6 +647,7 @@ anz der Bedingungen ist erforderlich Beitreten Heute Inhaltswarnung + Warnung: %1$s Dieser Beitrag enthält sensiblen Inhalt, den einige Personen als beleidigend oder verstörend empfinden könnten Sensiblen Inhalt immer ausblenden Sensiblen Inhalt immer anzeigen @@ -832,6 +835,7 @@ anz der Bedingungen ist erforderlich Standort wird geladen Keine Standortberechtigungen Fügt eine Warnung für sensiblen Inhalt hinzu, bevor Ihr Inhalt angezeigt wird. Dies ist ideal für NSFW-Inhalte (nicht sicher für die Arbeit) oder Inhalte, die manche Menschen als anstößig oder verstörend empfinden könnten + Grund (optional) Neues Feature Um diesen Modus zu aktivieren, muss Amethyst eine NIP-17-Nachricht senden (GiftWrapped, Versiegelte Direkt- und Gruppennachrichten). NIP-17 ist neu und die meisten Clients haben es noch nicht implementiert. Stellen Sie sicher, dass der Empfänger einen kompatiblen Client verwendet. Aktivieren @@ -1047,6 +1051,7 @@ anz der Bedingungen ist erforderlich Kurzfilme Schach Sicherheitsfilter + Folgeliste importieren Neuer Beitrag Neue Kurzfilme: Bilder oder Videos Neue Community-Notiz @@ -1476,4 +1481,31 @@ anz der Bedingungen ist erforderlich Sprachnachricht Sprachantwort Wiki + Starte mit einem großartigen Feed, indem du dieselben Personen folgst wie jemand, dem du vertraust. + Folgeliste importieren + Benutzer zum Folgen auswählen + Profil, aus dem importiert werden soll + Namenssuche, npub1…, alice@example.com + Unterstützt npub, NIP-05, Hex und Namecoin (.bit / d/ / id/) + Folgeliste nachschlagen + Tipp + %1$d Konten gefunden + %1$d ausgewählt + Über Namecoin aufgelöst + Folge jetzt %1$d Konten + Dein Feed ist bereit. + Überspringen + Weiteres suchen + %1$d Konten folgen + Weitere importieren + Weiter + Vorerst überspringen + Löse %1$s auf… + Folgeliste wird abgerufen… + Keine Follows gefunden + Folge %1$d Konten… + "Gib das Profil eines Freundes oder Community-Leaders ein. Du kannst deren npub, NIP-05-Adresse oder einen Namecoin-Namen wie alice@example.bit oder id/alice für blockchain-verifizierte Identitäten verwenden." + Alle auswählen + %1$d%% Verfügbarkeit + Namecoin-Einstellungen diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index a444c2d192..7a20de62c0 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -125,6 +125,7 @@ Bytes Erro Erros + Porcentagem de conexões bem-sucedidas ao relay O número de erros de conexão nesta sessão Feed principal Feed de mensagens privadas @@ -150,6 +151,7 @@ Pronomes Endereço LN LN URL (desatualizado) + Salvar no celular Salvar na Galeria Imagem salva para a galeria O download do vídeo foi iniciado… @@ -640,6 +642,7 @@ Entrar Hoje Aviso de conteúdo + Aviso: %1$s Esta nota contém conteúdo sensível que algumas pessoas podem achar ofensivo ou perturbador Sempre ocultar conteúdo sensível Sempre mostrar conteúdo sensível @@ -827,6 +830,7 @@ Carregando localização Sem permissões para localização Adiciona aviso de conteúdo sensível antes de mostrar seu conteúdo. Isso é ideal para qualquer conteúdo NSFW ou conteúdo que algumas pessoas possam considerar ofensivo ou perturbador + Motivo (opcional) Novo recurso Ativando este modo requer o Amethyst para enviar uma mensagem de NIP-17 (GiftWrapped, Sealed Direct and Group Messages). NIP-17 é novo e a maioria dos clientes ainda não o implementaram. Certifique-se de que o destinatário está usando um cliente compatível. Ativar @@ -1042,6 +1046,7 @@ Vídeos Curtos Xadrez Filtros de Segurança + Importar Seguidos Novo Post Novos Vídeos Curtos: imagens ou vídeos Nova Nota da Comunidade @@ -1471,4 +1476,31 @@ Mensagem de voz Resposta de voz Wiki + Comece com um ótimo feed seguindo as mesmas pessoas que alguém em quem você confia. + Importar lista de seguidos + Selecionar usuários para seguir + Perfil para importar + busca por nome, npub1…, alice@example.com + Suporta npub, NIP-05, hex e Namecoin (.bit / d/ / id/) + Buscar lista de seguidos + Dica + %1$d contas encontradas + %1$d selecionados + Resolvido via Namecoin + Seguindo %1$d contas agora + Seu feed está pronto. + Pular + Buscar outro + Seguir %1$d contas + Importar mais + Continuar + Pular por agora + Resolvendo %1$s… + Buscando lista de seguidos… + Nenhum seguido encontrado + Seguindo %1$d contas… + "Digite o perfil de um amigo ou líder de comunidade. Você pode usar o npub, endereço NIP-05 ou um nome Namecoin como alice@example.bit ou id/alice para identidades verificadas por blockchain." + Selecionar tudo + %1$d%% de disponibilidade + Configurações do Namecoin diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 2d63930a80..64f7432515 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -125,6 +125,7 @@ Bytes Fel Fel + Andel lyckade anslutningar till reläet Antal anslutningsfel under denna session Hem Flöde Privata Meddelande Flöde @@ -150,6 +151,7 @@ Pronomen LN Adress LN URL (Utdaterad) + Spara till telefon Spara i Galleri Bild sparad till galleriet Nedladdning av video har startat… @@ -639,6 +641,7 @@ Gå med Idag Varning för innehåll + Varning: %1$s Det här inlägget innehåller känsligt innehåll som vissa människor kan tycka är stötande eller störande Dölj alltid känsligt innehåll Visa alltid känsligt innehåll @@ -826,6 +829,7 @@ Laddar position Inga platsbehörigheter Lägger till en varning för känsligt innehåll innan ditt innehåll visas. Detta är idealiskt för NSFW-innehåll (inte säkert för arbete) eller innehåll som vissa personer kan uppleva som stötande eller störande + Anledning (valfritt) Ny Funktion För att aktivera denna funktion kräver det att Amethyst skickar ett NIP-17 meddelande (GiftWrapped, Förseglade Direkta och Gruppmeddelanden). NIP-17 är nytt och de flesta klienter har ännu inte implementerat det. Se till att mottagaren använder en kompatibel klient. Aktivera @@ -1041,6 +1045,7 @@ Kortfilmer Schack Säkerhetsfilter + Importera följare Nytt inlägg Nya kort: bilder eller videor Nytt Community-meddelande @@ -1470,4 +1475,31 @@ Röstmeddelande Röstsvar Wiki + Kom igång med ett bra flöde genom att följa samma personer som någon du litar på. + Importera följarlista + Välj användare att följa + Profil att importera från + namnsökning, npub1…, alice@example.com + Stöder npub, NIP-05, hex och Namecoin (.bit / d/ / id/) + Slå upp följarlista + Tips + %1$d konton hittades + %1$d valda + Löst via Namecoin + Följer nu %1$d konton + Ditt flöde är klart. + Hoppa över + Sök en till + Följ %1$d konton + Importera fler + Fortsätt + Hoppa över för nu + Löser upp %1$s… + Hämtar följarlista… + Inga följare hittades + Följer %1$d konton… + "Ange profilen för en vän eller gemenskapsledare. Du kan använda deras npub, NIP-05-adress eller ett Namecoin-namn som alice@example.bit eller id/alice för blockchain-verifierade identiteter." + Välj alla + %1$d%% drifttid + Namecoin-inställningar diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index ddebae33d0..a0630ce1df 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -646,6 +646,7 @@ 加入 今天 内容警告 + 警告: %1$s 这个帖子包含敏感内容,一些人可能会觉得有冒犯性或令人不安 始终隐藏敏感内容 始终显示敏感内容 @@ -833,6 +834,7 @@ 加载位置中 没有位置信息权限 在显示你的内容之前添加敏感的内容警告。针对任何 NSFW 内容或一些人可能觉得有冒犯性或令人不安的内容。 + 原因 (可选) 新功能 启用此模式需要 Amethyst 发送一条 NIP-17 消息(包装的、密封的私信和群聊消息)。因为 NIP-17 是新的,大多数客户端尚未执行。请确保接收方正在使用兼容的客户端。 启用 @@ -1505,4 +1507,5 @@ "输入好友或社区领袖的配置文件。您可以使用他们的 npub、NIP-05地址或Namecoin 名称,例如alice@example.bit 或区块链验证的地址 id/alice。" 全选 %1$d%% 运行时间 + Namecoin 设置 From d957efb1b882be956c947cc0e375a78f58074264 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 16:55:19 +0100 Subject: [PATCH 024/172] Foundations(iOS Sourceset): Streamline libsodium bindings configuration. --- quartz/build.gradle.kts | 106 +++++++++++++--------------------------- 1 file changed, 35 insertions(+), 71 deletions(-) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 9f6cf2b360..56e5e0b9fa 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -62,12 +62,30 @@ kotlin { // project can be found here: // https://developer.android.com/kotlin/multiplatform/migrate val xcfName = "quartz-kmpKit" + val libsodiumPath = project.file("src/nativeInterop/libsodium").toString() listOf( iosArm64(), iosX64(), iosSimulatorArm64(), ).forEach { target -> + + target.compilations.getByName("main") { + val headerFilesPath = "$libsodiumPath/include/sodium" + val Clibsodium by cinterops.creating { + definitionFile.set(project.file("Clibsodium.def")) + includeDirs("${libsodiumPath}/ios-simulators/lib") + //compilerOpts("-I${libsodiumPath}/ios-simulators/lib") + + + headers( + "$headerFilesPath/crypto_aead_xchacha20poly1305.h", + "$headerFilesPath/crypto_core_hchacha20.h", + "$headerFilesPath/crypto_stream_chacha20.h" + ) + } + } + target.swiftPackageConfig(cinteropName = "swiftbridge") { minIos = "17" minMacos = "14" @@ -84,75 +102,40 @@ kotlin { } } - val libsodiumPath = project.file("src/nativeInterop/libsodium").toString() - iosX64 { - compilations.getByName("main") { - val headerFilesPath = "$libsodiumPath/include/sodium" - val Clibsodium by cinterops.creating { - definitionFile.set(project.file("Clibsodium.def")) - includeDirs("$libsodiumPath/include/sodium") - - headers( - "$headerFilesPath/crypto_aead_xchacha20poly1305.h", - "$headerFilesPath/crypto_core_hchacha20.h", - "$headerFilesPath/crypto_stream_chacha20.h" - ) - } - } - binaries.all { - //linkerOpts("-L${libsodiumPath}/ios/lib", "-llibsodium") - } binaries.framework { baseName = xcfName } } iosArm64 { - compilations.getByName("main") { - val headerFilesPath = "$libsodiumPath/include/sodium" - val Clibsodium by cinterops.creating { - definitionFile.set(project.file("Clibsodium.def")) - includeDirs("$libsodiumPath/include/sodium") - - headers( - "$headerFilesPath/crypto_aead_xchacha20poly1305.h", - "$headerFilesPath/crypto_core_hchacha20.h", - "$headerFilesPath/crypto_stream_chacha20.h" - ) - } - } - binaries.all { - //linkerOpts("-L${libsodiumPath}/ios/lib", "-llibsodium") - } binaries.framework { baseName = xcfName } } iosSimulatorArm64 { - compilations.getByName("main") { - val headerFilesPath = "$libsodiumPath/include/sodium" - val Clibsodium by cinterops.creating { - definitionFile.set(project.file("Clibsodium.def")) - includeDirs("${libsodiumPath}/ios-simulators/lib") - //compilerOpts("-I${libsodiumPath}/ios-simulators/lib") - - - headers( - "$headerFilesPath/crypto_aead_xchacha20poly1305.h", - "$headerFilesPath/crypto_core_hchacha20.h", - "$headerFilesPath/crypto_stream_chacha20.h" - ) - } - } - binaries.all { - //linkerOpts("-L${libsodiumPath}/ios-simulators/lib", "-llibsodium-simulator") - } binaries.framework { baseName = xcfName } } + // This generates the Libsodium definition file, necessary for creating native bindings(a Kotlin API) for libsodium(for iOS). + val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { + val libsodiumPath = project.file("src/nativeInterop/libsodium").toString() + val definitionFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") + definitionFile.writeText("package = Clibsodium") + definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") + definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") + + doLast { + executionResult.get().assertNormalExitValue() + } + } + + tasks.withType().configureEach { +// doFirst { libsodiumDefFileGeneration.get() } + dependsOn(libsodiumDefFileGeneration) + } // This makes sure that the resource file directory is visible for iOS tests. val rootDir = "${rootProject.rootDir.path}/quartz/src/iosTest/resources" @@ -375,22 +358,3 @@ mavenPublishing { } } } - -tasks.register("GenerateSodiumCinteropFile") { - val libsodiumPath = project.file("src/nativeInterop/libsodium").toString() - val definitionFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") - definitionFile.writeText("package = Clibsodium") - definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") - definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") - - doLast { - executionResult.get().assertNormalExitValue() - } -} - -tasks.withType().configureEach { - val fileDefinition = project.file("src/nativeInterop/cinterop/Clibsodium.def") - if (!fileDefinition.exists()) { - dependsOn("GenerateSodiumCinteropFile") - } -} From 0b928d29a6acda5984d1ab3ad1b7292a69ee3ac5 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 20:28:02 +0100 Subject: [PATCH 025/172] Make the libsodium cinterop file generation more reliable. Add in(and comment out) some test code for extracting the libsodium headers from zip file. --- quartz/build.gradle.kts | 48 ++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 56e5e0b9fa..3f08b6634e 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -5,7 +5,6 @@ import io.github.frankois944.spmForKmp.swiftPackageConfig import io.github.frankois944.spmForKmp.utils.ExperimentalSpmForKmpFeature import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest -import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile plugins { alias(libs.plugins.kotlinMultiplatform) @@ -62,7 +61,24 @@ kotlin { // project can be found here: // https://developer.android.com/kotlin/multiplatform/migrate val xcfName = "quartz-kmpKit" - val libsodiumPath = project.file("src/nativeInterop/libsodium").toString() + val libsodiumPath = project.file("src/nativeInterop/libsodium") + val libsodiumHeaderFilesPath = project.file("$libsodiumPath/include/sodium") + + // This generates the Libsodium definition file, necessary for creating native bindings(a Kotlin API) for libsodium(for iOS). +// val headersTask = tasks.register("ExtractLibsodiumHeaders") { +// val unpackDirectory = "$libsodiumPath/include" +// unzipTo( +// project.file(unpackDirectory), +// project.file("$unpackDirectory/sodium.zip") +// ) +// } + + val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { + val definitionFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") + definitionFile.writeText("package = Clibsodium") + definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") + definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") + } listOf( iosArm64(), @@ -71,17 +87,18 @@ kotlin { ).forEach { target -> target.compilations.getByName("main") { - val headerFilesPath = "$libsodiumPath/include/sodium" +// headersTask.get() + libsodiumDefFileGeneration.get() val Clibsodium by cinterops.creating { + assert(project.file("Clibsodium.def").exists()) definitionFile.set(project.file("Clibsodium.def")) includeDirs("${libsodiumPath}/ios-simulators/lib") //compilerOpts("-I${libsodiumPath}/ios-simulators/lib") - headers( - "$headerFilesPath/crypto_aead_xchacha20poly1305.h", - "$headerFilesPath/crypto_core_hchacha20.h", - "$headerFilesPath/crypto_stream_chacha20.h" + "$libsodiumHeaderFilesPath/crypto_aead_xchacha20poly1305.h", + "$libsodiumHeaderFilesPath/crypto_core_hchacha20.h", + "$libsodiumHeaderFilesPath/crypto_stream_chacha20.h" ) } } @@ -119,23 +136,6 @@ kotlin { baseName = xcfName } } - // This generates the Libsodium definition file, necessary for creating native bindings(a Kotlin API) for libsodium(for iOS). - val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { - val libsodiumPath = project.file("src/nativeInterop/libsodium").toString() - val definitionFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") - definitionFile.writeText("package = Clibsodium") - definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") - definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") - - doLast { - executionResult.get().assertNormalExitValue() - } - } - - tasks.withType().configureEach { -// doFirst { libsodiumDefFileGeneration.get() } - dependsOn(libsodiumDefFileGeneration) - } // This makes sure that the resource file directory is visible for iOS tests. val rootDir = "${rootProject.rootDir.path}/quartz/src/iosTest/resources" From 750bedbabd482cd8af26d8ee2d9682e1b82db6b1 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 21:06:02 +0100 Subject: [PATCH 026/172] Foundations(iOS Sourceset): Reduce the header files only to those needed, plus utilities. --- .../libsodium/include/sodium/core.h | 28 --- .../include/sodium/crypto_aead_aegis128l.h | 92 --------- .../include/sodium/crypto_aead_aegis256.h | 92 --------- .../include/sodium/crypto_aead_aes256gcm.h | 179 ----------------- .../sodium/crypto_aead_chacha20poly1305.h | 180 ------------------ .../libsodium/include/sodium/crypto_auth.h | 46 ----- .../include/sodium/crypto_auth_hmacsha256.h | 70 ------- .../include/sodium/crypto_auth_hmacsha512.h | 68 ------- .../sodium/crypto_auth_hmacsha512256.h | 65 ------- .../libsodium/include/sodium/crypto_box.h | 177 ----------------- .../crypto_box_curve25519xchacha20poly1305.h | 164 ---------------- .../crypto_box_curve25519xsalsa20poly1305.h | 112 ----------- .../include/sodium/crypto_core_ed25519.h | 104 ---------- .../include/sodium/crypto_core_hsalsa20.h | 36 ---- .../include/sodium/crypto_core_keccak1600.h | 66 ------- .../include/sodium/crypto_core_ristretto255.h | 100 ---------- .../include/sodium/crypto_core_salsa20.h | 36 ---- .../include/sodium/crypto_core_salsa2012.h | 36 ---- .../include/sodium/crypto_core_salsa208.h | 40 ---- .../include/sodium/crypto_generichash.h | 84 -------- .../sodium/crypto_generichash_blake2b.h | 122 ------------ .../libsodium/include/sodium/crypto_hash.h | 40 ---- .../include/sodium/crypto_hash_sha256.h | 60 ------ .../include/sodium/crypto_hash_sha512.h | 60 ------ .../libsodium/include/sodium/crypto_ipcrypt.h | 132 ------------- .../libsodium/include/sodium/crypto_kdf.h | 53 ------ .../include/sodium/crypto_kdf_blake2b.h | 44 ----- .../include/sodium/crypto_kdf_hkdf_sha256.h | 74 ------- .../include/sodium/crypto_kdf_hkdf_sha512.h | 75 -------- .../libsodium/include/sodium/crypto_kx.h | 66 ------- .../include/sodium/crypto_onetimeauth.h | 65 ------- .../sodium/crypto_onetimeauth_poly1305.h | 72 ------- .../libsodium/include/sodium/crypto_pwhash.h | 147 -------------- .../include/sodium/crypto_pwhash_argon2i.h | 122 ------------ .../include/sodium/crypto_pwhash_argon2id.h | 122 ------------ .../crypto_pwhash_scryptsalsa208sha256.h | 120 ------------ .../include/sodium/crypto_scalarmult.h | 46 ----- .../sodium/crypto_scalarmult_curve25519.h | 42 ---- .../sodium/crypto_scalarmult_ed25519.h | 51 ----- .../sodium/crypto_scalarmult_ristretto255.h | 43 ----- .../include/sodium/crypto_secretbox.h | 93 --------- .../crypto_secretbox_xchacha20poly1305.h | 70 ------- .../crypto_secretbox_xsalsa20poly1305.h | 69 ------- .../crypto_secretstream_xchacha20poly1305.h | 108 ----------- .../include/sodium/crypto_shorthash.h | 41 ---- .../sodium/crypto_shorthash_siphash24.h | 50 ----- .../libsodium/include/sodium/crypto_sign.h | 107 ----------- .../include/sodium/crypto_sign_ed25519.h | 124 ------------ .../crypto_sign_edwards25519sha512batch.h | 55 ------ .../libsodium/include/sodium/crypto_stream.h | 59 ------ .../include/sodium/crypto_stream_salsa20.h | 61 ------ .../include/sodium/crypto_stream_salsa2012.h | 53 ------ .../include/sodium/crypto_stream_salsa208.h | 56 ------ .../include/sodium/crypto_stream_xchacha20.h | 61 ------ .../include/sodium/crypto_stream_xsalsa20.h | 61 ------ .../include/sodium/crypto_verify_16.h | 23 --- .../include/sodium/crypto_verify_32.h | 23 --- .../include/sodium/crypto_verify_64.h | 23 --- .../include/sodium/crypto_xof_shake128.h | 55 ------ .../include/sodium/crypto_xof_shake256.h | 54 ------ .../include/sodium/crypto_xof_turboshake128.h | 54 ------ .../include/sodium/crypto_xof_turboshake256.h | 54 ------ .../libsodium/include/sodium/randombytes.h | 72 ------- .../sodium/randombytes_internal_random.h | 22 --- .../include/sodium/randombytes_sysrandom.h | 19 -- 65 files changed, 4798 deletions(-) delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/core.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis128l.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis256.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aes256gcm.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_chacha20poly1305.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha256.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512256.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_box.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xchacha20poly1305.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xsalsa20poly1305.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ed25519.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hsalsa20.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_keccak1600.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ristretto255.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa20.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa2012.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa208.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash_blake2b.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha256.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha512.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_ipcrypt.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_blake2b.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha256.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha512.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_kx.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth_poly1305.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2i.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2id.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_scryptsalsa208sha256.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_curve25519.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ed25519.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ristretto255.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xchacha20poly1305.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xsalsa20poly1305.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretstream_xchacha20poly1305.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash_siphash24.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_ed25519.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_edwards25519sha512batch.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa20.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa2012.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa208.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xchacha20.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xsalsa20.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_16.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_32.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_64.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake128.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake256.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake128.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake256.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/randombytes.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/randombytes_internal_random.h delete mode 100644 quartz/src/nativeInterop/libsodium/include/sodium/randombytes_sysrandom.h diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/core.h b/quartz/src/nativeInterop/libsodium/include/sodium/core.h deleted file mode 100644 index dd088d2cae..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/core.h +++ /dev/null @@ -1,28 +0,0 @@ - -#ifndef sodium_core_H -#define sodium_core_H - -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -SODIUM_EXPORT -int sodium_init(void) - __attribute__ ((warn_unused_result)); - -/* ---- */ - -SODIUM_EXPORT -int sodium_set_misuse_handler(void (*handler)(void)); - -SODIUM_EXPORT -void sodium_misuse(void) - __attribute__ ((noreturn)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis128l.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis128l.h deleted file mode 100644 index 0ad019fc94..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis128l.h +++ /dev/null @@ -1,92 +0,0 @@ -#ifndef crypto_aead_aegis128l_H -#define crypto_aead_aegis128l_H - -#include - -#include "export.h" - -#ifdef __cplusplus -#ifdef __GNUC__ -#pragma GCC diagnostic ignored "-Wlong-long" -#endif -extern "C" { -#endif - -#define crypto_aead_aegis128l_KEYBYTES 16U -SODIUM_EXPORT -size_t crypto_aead_aegis128l_keybytes(void); - -#define crypto_aead_aegis128l_NSECBYTES 0U -SODIUM_EXPORT -size_t crypto_aead_aegis128l_nsecbytes(void); - -#define crypto_aead_aegis128l_NPUBBYTES 16U -SODIUM_EXPORT -size_t crypto_aead_aegis128l_npubbytes(void); - -#define crypto_aead_aegis128l_ABYTES 32U -SODIUM_EXPORT -size_t crypto_aead_aegis128l_abytes(void); - -#define crypto_aead_aegis128l_MESSAGEBYTES_MAX \ - SODIUM_MIN(SODIUM_SIZE_MAX - crypto_aead_aegis128l_ABYTES, (1ULL << 61) - 1) -SODIUM_EXPORT -size_t crypto_aead_aegis128l_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_aead_aegis128l_encrypt(unsigned char *c, - unsigned long long *clen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) __attribute__((nonnull(1, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_aegis128l_decrypt(unsigned char *m, - unsigned long long *mlen_p, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) __attribute__((warn_unused_result)) -__attribute__((nonnull(4, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_aegis128l_encrypt_detached(unsigned char *c, - unsigned char *mac, - unsigned long long *maclen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) - __attribute__((nonnull(1, 2, 9, 10))); - -SODIUM_EXPORT -int crypto_aead_aegis128l_decrypt_detached(unsigned char *m, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *mac, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) - __attribute__((warn_unused_result)) __attribute__((nonnull(3, 5, 8, 9))); - -SODIUM_EXPORT -void crypto_aead_aegis128l_keygen(unsigned char k[crypto_aead_aegis128l_KEYBYTES]) - __attribute__((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis256.h deleted file mode 100644 index 26bd18ace1..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aegis256.h +++ /dev/null @@ -1,92 +0,0 @@ -#ifndef crypto_aead_aegis256_H -#define crypto_aead_aegis256_H - -#include - -#include "export.h" - -#ifdef __cplusplus -#ifdef __GNUC__ -#pragma GCC diagnostic ignored "-Wlong-long" -#endif -extern "C" { -#endif - -#define crypto_aead_aegis256_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_aead_aegis256_keybytes(void); - -#define crypto_aead_aegis256_NSECBYTES 0U -SODIUM_EXPORT -size_t crypto_aead_aegis256_nsecbytes(void); - -#define crypto_aead_aegis256_NPUBBYTES 32U -SODIUM_EXPORT -size_t crypto_aead_aegis256_npubbytes(void); - -#define crypto_aead_aegis256_ABYTES 32U -SODIUM_EXPORT -size_t crypto_aead_aegis256_abytes(void); - -#define crypto_aead_aegis256_MESSAGEBYTES_MAX \ - SODIUM_MIN(SODIUM_SIZE_MAX - crypto_aead_aegis256_ABYTES, (1ULL << 61) - 1) -SODIUM_EXPORT -size_t crypto_aead_aegis256_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_aead_aegis256_encrypt(unsigned char *c, - unsigned long long *clen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) __attribute__((nonnull(1, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_aegis256_decrypt(unsigned char *m, - unsigned long long *mlen_p, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) __attribute__((warn_unused_result)) -__attribute__((nonnull(4, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_aegis256_encrypt_detached(unsigned char *c, - unsigned char *mac, - unsigned long long *maclen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) - __attribute__((nonnull(1, 2, 9, 10))); - -SODIUM_EXPORT -int crypto_aead_aegis256_decrypt_detached(unsigned char *m, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *mac, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) - __attribute__((warn_unused_result)) __attribute__((nonnull(3, 5, 8, 9))); - -SODIUM_EXPORT -void crypto_aead_aegis256_keygen(unsigned char k[crypto_aead_aegis256_KEYBYTES]) - __attribute__((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aes256gcm.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aes256gcm.h deleted file mode 100644 index 9baeb3f19f..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_aes256gcm.h +++ /dev/null @@ -1,179 +0,0 @@ -#ifndef crypto_aead_aes256gcm_H -#define crypto_aead_aes256gcm_H - -/* - * WARNING: Despite being the most popular AEAD construction due to its - * use in TLS, safely using AES-GCM in a different context is tricky. - * - * No more than ~ 350 GB of input data should be encrypted with a given key. - * This is for ~ 16 KB messages -- Actual figures vary according to - * message sizes. - * - * In addition, nonces are short and repeated nonces would totally destroy - * the security of this scheme. - * - * Nonces should thus come from atomic counters, which can be difficult to - * set up in a distributed environment. - * - * Unless you absolutely need AES-GCM, use crypto_aead_xchacha20poly1305_ietf_*() - * instead. It doesn't have any of these limitations. - * Or, if you don't need to authenticate additional data, just stick to - * crypto_secretbox(). - */ - -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -SODIUM_EXPORT -int crypto_aead_aes256gcm_is_available(void); - -#define crypto_aead_aes256gcm_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_aead_aes256gcm_keybytes(void); - -#define crypto_aead_aes256gcm_NSECBYTES 0U -SODIUM_EXPORT -size_t crypto_aead_aes256gcm_nsecbytes(void); - -#define crypto_aead_aes256gcm_NPUBBYTES 12U -SODIUM_EXPORT -size_t crypto_aead_aes256gcm_npubbytes(void); - -#define crypto_aead_aes256gcm_ABYTES 16U -SODIUM_EXPORT -size_t crypto_aead_aes256gcm_abytes(void); - -#define crypto_aead_aes256gcm_MESSAGEBYTES_MAX \ - SODIUM_MIN(SODIUM_SIZE_MAX - crypto_aead_aes256gcm_ABYTES, \ - (16ULL * ((1ULL << 32) - 2ULL))) -SODIUM_EXPORT -size_t crypto_aead_aes256gcm_messagebytes_max(void); - -typedef struct CRYPTO_ALIGN(16) crypto_aead_aes256gcm_state_ { - unsigned char opaque[512]; -} crypto_aead_aes256gcm_state; - -SODIUM_EXPORT -size_t crypto_aead_aes256gcm_statebytes(void); - -SODIUM_EXPORT -int crypto_aead_aes256gcm_encrypt(unsigned char *c, - unsigned long long *clen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((nonnull(1, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_aes256gcm_decrypt(unsigned char *m, - unsigned long long *mlen_p, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_aes256gcm_encrypt_detached(unsigned char *c, - unsigned char *mac, - unsigned long long *maclen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((nonnull(1, 2, 9, 10))); - -SODIUM_EXPORT -int crypto_aead_aes256gcm_decrypt_detached(unsigned char *m, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *mac, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); - -/* -- Precomputation interface -- */ - -SODIUM_EXPORT -int crypto_aead_aes256gcm_beforenm(crypto_aead_aes256gcm_state *ctx_, - const unsigned char *k) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_aead_aes256gcm_encrypt_afternm(unsigned char *c, - unsigned long long *clen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const crypto_aead_aes256gcm_state *ctx_) - __attribute__ ((nonnull(1, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_aes256gcm_decrypt_afternm(unsigned char *m, - unsigned long long *mlen_p, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const crypto_aead_aes256gcm_state *ctx_) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_aes256gcm_encrypt_detached_afternm(unsigned char *c, - unsigned char *mac, - unsigned long long *maclen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const crypto_aead_aes256gcm_state *ctx_) - __attribute__ ((nonnull(1, 2, 9, 10))); - -SODIUM_EXPORT -int crypto_aead_aes256gcm_decrypt_detached_afternm(unsigned char *m, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *mac, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const crypto_aead_aes256gcm_state *ctx_) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); - -SODIUM_EXPORT -void crypto_aead_aes256gcm_keygen(unsigned char k[crypto_aead_aes256gcm_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_chacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_chacha20poly1305.h deleted file mode 100644 index 5d671df142..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_aead_chacha20poly1305.h +++ /dev/null @@ -1,180 +0,0 @@ -#ifndef crypto_aead_chacha20poly1305_H -#define crypto_aead_chacha20poly1305_H - -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -/* -- IETF ChaCha20-Poly1305 construction with a 96-bit nonce and a 32-bit internal counter -- */ - -#define crypto_aead_chacha20poly1305_ietf_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_ietf_keybytes(void); - -#define crypto_aead_chacha20poly1305_ietf_NSECBYTES 0U -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_ietf_nsecbytes(void); - -#define crypto_aead_chacha20poly1305_ietf_NPUBBYTES 12U - -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_ietf_npubbytes(void); - -#define crypto_aead_chacha20poly1305_ietf_ABYTES 16U -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_ietf_abytes(void); - -#define crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX \ - SODIUM_MIN(SODIUM_SIZE_MAX - crypto_aead_chacha20poly1305_ietf_ABYTES, \ - (64ULL * ((1ULL << 32) - 1ULL))) -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_ietf_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_aead_chacha20poly1305_ietf_encrypt(unsigned char *c, - unsigned long long *clen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((nonnull(1, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_chacha20poly1305_ietf_decrypt(unsigned char *m, - unsigned long long *mlen_p, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_chacha20poly1305_ietf_encrypt_detached(unsigned char *c, - unsigned char *mac, - unsigned long long *maclen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((nonnull(1, 2, 9, 10))); - -SODIUM_EXPORT -int crypto_aead_chacha20poly1305_ietf_decrypt_detached(unsigned char *m, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *mac, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); - -SODIUM_EXPORT -void crypto_aead_chacha20poly1305_ietf_keygen(unsigned char k[crypto_aead_chacha20poly1305_ietf_KEYBYTES]) - __attribute__ ((nonnull)); - -/* -- Original ChaCha20-Poly1305 construction with a 64-bit nonce and a 64-bit internal counter -- */ - -#define crypto_aead_chacha20poly1305_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_keybytes(void); - -#define crypto_aead_chacha20poly1305_NSECBYTES 0U -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_nsecbytes(void); - -#define crypto_aead_chacha20poly1305_NPUBBYTES 8U -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_npubbytes(void); - -#define crypto_aead_chacha20poly1305_ABYTES 16U -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_abytes(void); - -#define crypto_aead_chacha20poly1305_MESSAGEBYTES_MAX \ - (SODIUM_SIZE_MAX - crypto_aead_chacha20poly1305_ABYTES) -SODIUM_EXPORT -size_t crypto_aead_chacha20poly1305_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_aead_chacha20poly1305_encrypt(unsigned char *c, - unsigned long long *clen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((nonnull(1, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_chacha20poly1305_decrypt(unsigned char *m, - unsigned long long *mlen_p, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(4, 8, 9))); - -SODIUM_EXPORT -int crypto_aead_chacha20poly1305_encrypt_detached(unsigned char *c, - unsigned char *mac, - unsigned long long *maclen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((nonnull(1, 2, 9, 10))); - -SODIUM_EXPORT -int crypto_aead_chacha20poly1305_decrypt_detached(unsigned char *m, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *mac, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5, 8, 9))); - -SODIUM_EXPORT -void crypto_aead_chacha20poly1305_keygen(unsigned char k[crypto_aead_chacha20poly1305_KEYBYTES]) - __attribute__ ((nonnull)); - -/* Aliases */ - -#define crypto_aead_chacha20poly1305_IETF_KEYBYTES crypto_aead_chacha20poly1305_ietf_KEYBYTES -#define crypto_aead_chacha20poly1305_IETF_NSECBYTES crypto_aead_chacha20poly1305_ietf_NSECBYTES -#define crypto_aead_chacha20poly1305_IETF_NPUBBYTES crypto_aead_chacha20poly1305_ietf_NPUBBYTES -#define crypto_aead_chacha20poly1305_IETF_ABYTES crypto_aead_chacha20poly1305_ietf_ABYTES -#define crypto_aead_chacha20poly1305_IETF_MESSAGEBYTES_MAX crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth.h deleted file mode 100644 index 540aee0e8d..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef crypto_auth_H -#define crypto_auth_H - -#include - -#include "crypto_auth_hmacsha512256.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_auth_BYTES crypto_auth_hmacsha512256_BYTES -SODIUM_EXPORT -size_t crypto_auth_bytes(void); - -#define crypto_auth_KEYBYTES crypto_auth_hmacsha512256_KEYBYTES -SODIUM_EXPORT -size_t crypto_auth_keybytes(void); - -#define crypto_auth_PRIMITIVE "hmacsha512256" -SODIUM_EXPORT -const char *crypto_auth_primitive(void); - -SODIUM_EXPORT -int crypto_auth(unsigned char *out, const unsigned char *in, - unsigned long long inlen, const unsigned char *k) - __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_auth_verify(const unsigned char *h, const unsigned char *in, - unsigned long long inlen, const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -void crypto_auth_keygen(unsigned char k[crypto_auth_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha256.h deleted file mode 100644 index 3da864c7d2..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha256.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef crypto_auth_hmacsha256_H -#define crypto_auth_hmacsha256_H - -#include -#include "crypto_hash_sha256.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_auth_hmacsha256_BYTES 32U -SODIUM_EXPORT -size_t crypto_auth_hmacsha256_bytes(void); - -#define crypto_auth_hmacsha256_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_auth_hmacsha256_keybytes(void); - -SODIUM_EXPORT -int crypto_auth_hmacsha256(unsigned char *out, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *k) __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_auth_hmacsha256_verify(const unsigned char *h, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); - -/* ------------------------------------------------------------------------- */ - -typedef struct crypto_auth_hmacsha256_state { - crypto_hash_sha256_state ictx; - crypto_hash_sha256_state octx; -} crypto_auth_hmacsha256_state; - -SODIUM_EXPORT -size_t crypto_auth_hmacsha256_statebytes(void); - -SODIUM_EXPORT -int crypto_auth_hmacsha256_init(crypto_auth_hmacsha256_state *state, - const unsigned char *key, - size_t keylen) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_auth_hmacsha256_update(crypto_auth_hmacsha256_state *state, - const unsigned char *in, - unsigned long long inlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_auth_hmacsha256_final(crypto_auth_hmacsha256_state *state, - unsigned char *out) __attribute__ ((nonnull)); - - -SODIUM_EXPORT -void crypto_auth_hmacsha256_keygen(unsigned char k[crypto_auth_hmacsha256_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512.h deleted file mode 100644 index d992cb8163..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512.h +++ /dev/null @@ -1,68 +0,0 @@ -#ifndef crypto_auth_hmacsha512_H -#define crypto_auth_hmacsha512_H - -#include -#include "crypto_hash_sha512.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_auth_hmacsha512_BYTES 64U -SODIUM_EXPORT -size_t crypto_auth_hmacsha512_bytes(void); - -#define crypto_auth_hmacsha512_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_auth_hmacsha512_keybytes(void); - -SODIUM_EXPORT -int crypto_auth_hmacsha512(unsigned char *out, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *k) __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_auth_hmacsha512_verify(const unsigned char *h, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); - -/* ------------------------------------------------------------------------- */ - -typedef struct crypto_auth_hmacsha512_state { - crypto_hash_sha512_state ictx; - crypto_hash_sha512_state octx; -} crypto_auth_hmacsha512_state; - -SODIUM_EXPORT -size_t crypto_auth_hmacsha512_statebytes(void); - -SODIUM_EXPORT -int crypto_auth_hmacsha512_init(crypto_auth_hmacsha512_state *state, - const unsigned char *key, - size_t keylen) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_auth_hmacsha512_update(crypto_auth_hmacsha512_state *state, - const unsigned char *in, - unsigned long long inlen) __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_auth_hmacsha512_final(crypto_auth_hmacsha512_state *state, - unsigned char *out) __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_auth_hmacsha512_keygen(unsigned char k[crypto_auth_hmacsha512_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512256.h deleted file mode 100644 index 3fb5263892..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_auth_hmacsha512256.h +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef crypto_auth_hmacsha512256_H -#define crypto_auth_hmacsha512256_H - -#include -#include "crypto_auth_hmacsha512.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_auth_hmacsha512256_BYTES 32U -SODIUM_EXPORT -size_t crypto_auth_hmacsha512256_bytes(void); - -#define crypto_auth_hmacsha512256_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_auth_hmacsha512256_keybytes(void); - -SODIUM_EXPORT -int crypto_auth_hmacsha512256(unsigned char *out, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *k) __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_auth_hmacsha512256_verify(const unsigned char *h, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); - -/* ------------------------------------------------------------------------- */ - -typedef crypto_auth_hmacsha512_state crypto_auth_hmacsha512256_state; - -SODIUM_EXPORT -size_t crypto_auth_hmacsha512256_statebytes(void); - -SODIUM_EXPORT -int crypto_auth_hmacsha512256_init(crypto_auth_hmacsha512256_state *state, - const unsigned char *key, - size_t keylen) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_auth_hmacsha512256_update(crypto_auth_hmacsha512256_state *state, - const unsigned char *in, - unsigned long long inlen) __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_auth_hmacsha512256_final(crypto_auth_hmacsha512256_state *state, - unsigned char *out) __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_auth_hmacsha512256_keygen(unsigned char k[crypto_auth_hmacsha512256_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box.h deleted file mode 100644 index e060dd29fc..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box.h +++ /dev/null @@ -1,177 +0,0 @@ -#ifndef crypto_box_H -#define crypto_box_H - -/* - * THREAD SAFETY: crypto_box_keypair() is thread-safe, - * provided that sodium_init() was called before. - * - * Other functions are always thread-safe. - */ - -#include - -#include "crypto_box_curve25519xsalsa20poly1305.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_box_SEEDBYTES crypto_box_curve25519xsalsa20poly1305_SEEDBYTES -SODIUM_EXPORT -size_t crypto_box_seedbytes(void); - -#define crypto_box_PUBLICKEYBYTES crypto_box_curve25519xsalsa20poly1305_PUBLICKEYBYTES -SODIUM_EXPORT -size_t crypto_box_publickeybytes(void); - -#define crypto_box_SECRETKEYBYTES crypto_box_curve25519xsalsa20poly1305_SECRETKEYBYTES -SODIUM_EXPORT -size_t crypto_box_secretkeybytes(void); - -#define crypto_box_NONCEBYTES crypto_box_curve25519xsalsa20poly1305_NONCEBYTES -SODIUM_EXPORT -size_t crypto_box_noncebytes(void); - -#define crypto_box_MACBYTES crypto_box_curve25519xsalsa20poly1305_MACBYTES -SODIUM_EXPORT -size_t crypto_box_macbytes(void); - -#define crypto_box_MESSAGEBYTES_MAX crypto_box_curve25519xsalsa20poly1305_MESSAGEBYTES_MAX -SODIUM_EXPORT -size_t crypto_box_messagebytes_max(void); - -#define crypto_box_PRIMITIVE "curve25519xsalsa20poly1305" -SODIUM_EXPORT -const char *crypto_box_primitive(void); - -SODIUM_EXPORT -int crypto_box_seed_keypair(unsigned char *pk, unsigned char *sk, - const unsigned char *seed) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_box_keypair(unsigned char *pk, unsigned char *sk) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_box_easy(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *pk, const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4, 5, 6))); - -SODIUM_EXPORT -int crypto_box_open_easy(unsigned char *m, const unsigned char *c, - unsigned long long clen, const unsigned char *n, - const unsigned char *pk, const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5, 6))); - -SODIUM_EXPORT -int crypto_box_detached(unsigned char *c, unsigned char *mac, - const unsigned char *m, unsigned long long mlen, - const unsigned char *n, const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 2, 5, 6, 7))); - -SODIUM_EXPORT -int crypto_box_open_detached(unsigned char *m, const unsigned char *c, - const unsigned char *mac, - unsigned long long clen, - const unsigned char *n, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6, 7))); - -/* -- Precomputation interface -- */ - -#define crypto_box_BEFORENMBYTES crypto_box_curve25519xsalsa20poly1305_BEFORENMBYTES -SODIUM_EXPORT -size_t crypto_box_beforenmbytes(void); - -SODIUM_EXPORT -int crypto_box_beforenm(unsigned char *k, const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_box_easy_afternm(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_box_open_easy_afternm(unsigned char *m, const unsigned char *c, - unsigned long long clen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -SODIUM_EXPORT -int crypto_box_detached_afternm(unsigned char *c, unsigned char *mac, - const unsigned char *m, unsigned long long mlen, - const unsigned char *n, const unsigned char *k) - __attribute__ ((nonnull(1, 2, 5, 6))); - -SODIUM_EXPORT -int crypto_box_open_detached_afternm(unsigned char *m, const unsigned char *c, - const unsigned char *mac, - unsigned long long clen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6))); - -/* -- Ephemeral SK interface -- */ - -#define crypto_box_SEALBYTES (crypto_box_PUBLICKEYBYTES + crypto_box_MACBYTES) -SODIUM_EXPORT -size_t crypto_box_sealbytes(void); - -SODIUM_EXPORT -int crypto_box_seal(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *pk) - __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_box_seal_open(unsigned char *m, const unsigned char *c, - unsigned long long clen, - const unsigned char *pk, const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -/* -- NaCl compatibility interface ; Requires padding -- */ - -#define crypto_box_ZEROBYTES crypto_box_curve25519xsalsa20poly1305_ZEROBYTES -SODIUM_EXPORT -size_t crypto_box_zerobytes(void); - -#define crypto_box_BOXZEROBYTES crypto_box_curve25519xsalsa20poly1305_BOXZEROBYTES -SODIUM_EXPORT -size_t crypto_box_boxzerobytes(void); - -SODIUM_EXPORT -int crypto_box(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *pk, const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4, 5, 6))); - -SODIUM_EXPORT -int crypto_box_open(unsigned char *m, const unsigned char *c, - unsigned long long clen, const unsigned char *n, - const unsigned char *pk, const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5, 6))); - -SODIUM_EXPORT -int crypto_box_afternm(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_box_open_afternm(unsigned char *m, const unsigned char *c, - unsigned long long clen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xchacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xchacha20poly1305.h deleted file mode 100644 index 26a3d31efa..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xchacha20poly1305.h +++ /dev/null @@ -1,164 +0,0 @@ - -#ifndef crypto_box_curve25519xchacha20poly1305_H -#define crypto_box_curve25519xchacha20poly1305_H - -#include -#include "crypto_stream_xchacha20.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_box_curve25519xchacha20poly1305_SEEDBYTES 32U -SODIUM_EXPORT -size_t crypto_box_curve25519xchacha20poly1305_seedbytes(void); - -#define crypto_box_curve25519xchacha20poly1305_PUBLICKEYBYTES 32U -SODIUM_EXPORT -size_t crypto_box_curve25519xchacha20poly1305_publickeybytes(void); - -#define crypto_box_curve25519xchacha20poly1305_SECRETKEYBYTES 32U -SODIUM_EXPORT -size_t crypto_box_curve25519xchacha20poly1305_secretkeybytes(void); - -#define crypto_box_curve25519xchacha20poly1305_BEFORENMBYTES 32U -SODIUM_EXPORT -size_t crypto_box_curve25519xchacha20poly1305_beforenmbytes(void); - -#define crypto_box_curve25519xchacha20poly1305_NONCEBYTES 24U -SODIUM_EXPORT -size_t crypto_box_curve25519xchacha20poly1305_noncebytes(void); - -#define crypto_box_curve25519xchacha20poly1305_MACBYTES 16U -SODIUM_EXPORT -size_t crypto_box_curve25519xchacha20poly1305_macbytes(void); - -#define crypto_box_curve25519xchacha20poly1305_MESSAGEBYTES_MAX \ - (crypto_stream_xchacha20_MESSAGEBYTES_MAX - crypto_box_curve25519xchacha20poly1305_MACBYTES) -SODIUM_EXPORT -size_t crypto_box_curve25519xchacha20poly1305_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_seed_keypair(unsigned char *pk, - unsigned char *sk, - const unsigned char *seed) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_keypair(unsigned char *pk, - unsigned char *sk) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_easy(unsigned char *c, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4, 5, 6))); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_open_easy(unsigned char *m, - const unsigned char *c, - unsigned long long clen, - const unsigned char *n, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5, 6))); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_detached(unsigned char *c, - unsigned char *mac, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 2, 5, 6, 7))); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_open_detached(unsigned char *m, - const unsigned char *c, - const unsigned char *mac, - unsigned long long clen, - const unsigned char *n, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6, 7))); - -/* -- Precomputation interface -- */ - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_beforenm(unsigned char *k, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_easy_afternm(unsigned char *c, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_open_easy_afternm(unsigned char *m, - const unsigned char *c, - unsigned long long clen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_detached_afternm(unsigned char *c, - unsigned char *mac, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 2, 5, 6))); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_open_detached_afternm(unsigned char *m, - const unsigned char *c, - const unsigned char *mac, - unsigned long long clen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6))); - -/* -- Ephemeral SK interface -- */ - -#define crypto_box_curve25519xchacha20poly1305_SEALBYTES \ - (crypto_box_curve25519xchacha20poly1305_PUBLICKEYBYTES + \ - crypto_box_curve25519xchacha20poly1305_MACBYTES) - -SODIUM_EXPORT -size_t crypto_box_curve25519xchacha20poly1305_sealbytes(void); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_seal(unsigned char *c, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *pk) - __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_box_curve25519xchacha20poly1305_seal_open(unsigned char *m, - const unsigned char *c, - unsigned long long clen, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xsalsa20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xsalsa20poly1305.h deleted file mode 100644 index e733f49995..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_box_curve25519xsalsa20poly1305.h +++ /dev/null @@ -1,112 +0,0 @@ -#ifndef crypto_box_curve25519xsalsa20poly1305_H -#define crypto_box_curve25519xsalsa20poly1305_H - -#include -#include "crypto_stream_xsalsa20.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_box_curve25519xsalsa20poly1305_SEEDBYTES 32U -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_seedbytes(void); - -#define crypto_box_curve25519xsalsa20poly1305_PUBLICKEYBYTES 32U -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_publickeybytes(void); - -#define crypto_box_curve25519xsalsa20poly1305_SECRETKEYBYTES 32U -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_secretkeybytes(void); - -#define crypto_box_curve25519xsalsa20poly1305_BEFORENMBYTES 32U -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_beforenmbytes(void); - -#define crypto_box_curve25519xsalsa20poly1305_NONCEBYTES 24U -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_noncebytes(void); - -#define crypto_box_curve25519xsalsa20poly1305_MACBYTES 16U -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_macbytes(void); - -/* Only for the libsodium API - The NaCl compatibility API would require BOXZEROBYTES extra bytes */ -#define crypto_box_curve25519xsalsa20poly1305_MESSAGEBYTES_MAX \ - (crypto_stream_xsalsa20_MESSAGEBYTES_MAX - crypto_box_curve25519xsalsa20poly1305_MACBYTES) -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_box_curve25519xsalsa20poly1305_seed_keypair(unsigned char *pk, - unsigned char *sk, - const unsigned char *seed) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_box_curve25519xsalsa20poly1305_keypair(unsigned char *pk, - unsigned char *sk) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_box_curve25519xsalsa20poly1305_beforenm(unsigned char *k, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -/* -- NaCl compatibility interface ; Requires padding -- */ - -#define crypto_box_curve25519xsalsa20poly1305_BOXZEROBYTES 16U -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_boxzerobytes(void); - -#define crypto_box_curve25519xsalsa20poly1305_ZEROBYTES \ - (crypto_box_curve25519xsalsa20poly1305_BOXZEROBYTES + \ - crypto_box_curve25519xsalsa20poly1305_MACBYTES) -SODIUM_EXPORT -size_t crypto_box_curve25519xsalsa20poly1305_zerobytes(void); - -SODIUM_EXPORT -int crypto_box_curve25519xsalsa20poly1305(unsigned char *c, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4, 5, 6))); - -SODIUM_EXPORT -int crypto_box_curve25519xsalsa20poly1305_open(unsigned char *m, - const unsigned char *c, - unsigned long long clen, - const unsigned char *n, - const unsigned char *pk, - const unsigned char *sk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5, 6))); - -SODIUM_EXPORT -int crypto_box_curve25519xsalsa20poly1305_afternm(unsigned char *c, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_box_curve25519xsalsa20poly1305_open_afternm(unsigned char *m, - const unsigned char *c, - unsigned long long clen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ed25519.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ed25519.h deleted file mode 100644 index 8f2eeee936..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ed25519.h +++ /dev/null @@ -1,104 +0,0 @@ -#ifndef crypto_core_ed25519_H -#define crypto_core_ed25519_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_core_ed25519_BYTES 32 -SODIUM_EXPORT -size_t crypto_core_ed25519_bytes(void); - -#define crypto_core_ed25519_UNIFORMBYTES 32 -SODIUM_EXPORT -size_t crypto_core_ed25519_uniformbytes(void); - -#define crypto_core_ed25519_HASHBYTES 64 -SODIUM_EXPORT -size_t crypto_core_ed25519_hashbytes(void); - -#define crypto_core_ed25519_SCALARBYTES 32 -SODIUM_EXPORT -size_t crypto_core_ed25519_scalarbytes(void); - -#define crypto_core_ed25519_NONREDUCEDSCALARBYTES 64 -SODIUM_EXPORT -size_t crypto_core_ed25519_nonreducedscalarbytes(void); - -SODIUM_EXPORT -int crypto_core_ed25519_is_valid_point(const unsigned char *p) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_core_ed25519_add(unsigned char *r, - const unsigned char *p, const unsigned char *q) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_core_ed25519_sub(unsigned char *r, - const unsigned char *p, const unsigned char *q) - __attribute__ ((nonnull)); - -/* - * Removal notice: this function is undocumented and has been marked as - * deprecated since 2019. It will be removed. Bindings should not refer to it. - */ -SODIUM_EXPORT -int crypto_core_ed25519_from_uniform(unsigned char *p, const unsigned char *r) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_core_ed25519_from_hash(unsigned char *p, const unsigned char *h) - __attribute__ ((nonnull)) __attribute__ ((deprecated)); - -SODIUM_EXPORT -void crypto_core_ed25519_random(unsigned char *p) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ed25519_scalar_random(unsigned char *r) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_core_ed25519_scalar_invert(unsigned char *recip, const unsigned char *s) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ed25519_scalar_negate(unsigned char *neg, const unsigned char *s) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ed25519_scalar_complement(unsigned char *comp, const unsigned char *s) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ed25519_scalar_add(unsigned char *z, const unsigned char *x, - const unsigned char *y) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ed25519_scalar_sub(unsigned char *z, const unsigned char *x, - const unsigned char *y) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ed25519_scalar_mul(unsigned char *z, const unsigned char *x, - const unsigned char *y) - __attribute__ ((nonnull)); - -/* - * The interval `s` is sampled from should be at least 317 bits to ensure almost - * uniformity of `r` over `L`. - */ -SODIUM_EXPORT -void crypto_core_ed25519_scalar_reduce(unsigned char *r, const unsigned char *s) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hsalsa20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hsalsa20.h deleted file mode 100644 index 4bf7a48786..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_hsalsa20.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef crypto_core_hsalsa20_H -#define crypto_core_hsalsa20_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_core_hsalsa20_OUTPUTBYTES 32U -SODIUM_EXPORT -size_t crypto_core_hsalsa20_outputbytes(void); - -#define crypto_core_hsalsa20_INPUTBYTES 16U -SODIUM_EXPORT -size_t crypto_core_hsalsa20_inputbytes(void); - -#define crypto_core_hsalsa20_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_core_hsalsa20_keybytes(void); - -#define crypto_core_hsalsa20_CONSTBYTES 16U -SODIUM_EXPORT -size_t crypto_core_hsalsa20_constbytes(void); - -SODIUM_EXPORT -int crypto_core_hsalsa20(unsigned char *out, const unsigned char *in, - const unsigned char *k, const unsigned char *c) - __attribute__ ((nonnull(1, 2, 3))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_keccak1600.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_keccak1600.h deleted file mode 100644 index 46e35ac883..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_keccak1600.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef crypto_core_keccak1600_H -#define crypto_core_keccak1600_H - -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#ifdef __IBMC__ -# pragma pack(1) -#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) -# pragma pack(1) -#else -# pragma pack(push, 1) -#endif - -typedef struct CRYPTO_ALIGN(16) crypto_core_keccak1600_state { - unsigned char opaque[224]; -} crypto_core_keccak1600_state; - -#ifdef __IBMC__ -# pragma pack(pop) -#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) -# pragma pack() -#else -# pragma pack(pop) -#endif - -SODIUM_EXPORT -size_t crypto_core_keccak1600_statebytes(void); - -SODIUM_EXPORT -void crypto_core_keccak1600_init(crypto_core_keccak1600_state *state) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_keccak1600_xor_bytes(crypto_core_keccak1600_state *state, - const unsigned char *bytes, - size_t offset, size_t length) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_keccak1600_extract_bytes(const crypto_core_keccak1600_state *state, - unsigned char *bytes, - size_t offset, size_t length) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_keccak1600_permute_24(crypto_core_keccak1600_state *state) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_keccak1600_permute_12(crypto_core_keccak1600_state *state) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ristretto255.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ristretto255.h deleted file mode 100644 index f2820e5576..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_ristretto255.h +++ /dev/null @@ -1,100 +0,0 @@ -#ifndef crypto_core_ristretto255_H -#define crypto_core_ristretto255_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_core_ristretto255_BYTES 32 -SODIUM_EXPORT -size_t crypto_core_ristretto255_bytes(void); - -#define crypto_core_ristretto255_HASHBYTES 64 -SODIUM_EXPORT -size_t crypto_core_ristretto255_hashbytes(void); - -#define crypto_core_ristretto255_SCALARBYTES 32 -SODIUM_EXPORT -size_t crypto_core_ristretto255_scalarbytes(void); - -#define crypto_core_ristretto255_NONREDUCEDSCALARBYTES 64 -SODIUM_EXPORT -size_t crypto_core_ristretto255_nonreducedscalarbytes(void); - -SODIUM_EXPORT -int crypto_core_ristretto255_is_valid_point(const unsigned char *p) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_core_ristretto255_add(unsigned char *r, - const unsigned char *p, const unsigned char *q) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_core_ristretto255_sub(unsigned char *r, - const unsigned char *p, const unsigned char *q) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_core_ristretto255_from_hash(unsigned char *p, - const unsigned char *r) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ristretto255_random(unsigned char *p) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ristretto255_scalar_random(unsigned char *r) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_core_ristretto255_scalar_invert(unsigned char *recip, - const unsigned char *s) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ristretto255_scalar_negate(unsigned char *neg, - const unsigned char *s) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ristretto255_scalar_complement(unsigned char *comp, - const unsigned char *s) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ristretto255_scalar_add(unsigned char *z, - const unsigned char *x, - const unsigned char *y) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ristretto255_scalar_sub(unsigned char *z, - const unsigned char *x, - const unsigned char *y) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_core_ristretto255_scalar_mul(unsigned char *z, - const unsigned char *x, - const unsigned char *y) - __attribute__ ((nonnull)); - -/* - * The interval `s` is sampled from should be at least 317 bits to ensure almost - * uniformity of `r` over `L`. - */ -SODIUM_EXPORT -void crypto_core_ristretto255_scalar_reduce(unsigned char *r, - const unsigned char *s) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa20.h deleted file mode 100644 index bd79fd9f54..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa20.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef crypto_core_salsa20_H -#define crypto_core_salsa20_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_core_salsa20_OUTPUTBYTES 64U -SODIUM_EXPORT -size_t crypto_core_salsa20_outputbytes(void); - -#define crypto_core_salsa20_INPUTBYTES 16U -SODIUM_EXPORT -size_t crypto_core_salsa20_inputbytes(void); - -#define crypto_core_salsa20_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_core_salsa20_keybytes(void); - -#define crypto_core_salsa20_CONSTBYTES 16U -SODIUM_EXPORT -size_t crypto_core_salsa20_constbytes(void); - -SODIUM_EXPORT -int crypto_core_salsa20(unsigned char *out, const unsigned char *in, - const unsigned char *k, const unsigned char *c) - __attribute__ ((nonnull(1, 2, 3))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa2012.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa2012.h deleted file mode 100644 index 05957591ca..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa2012.h +++ /dev/null @@ -1,36 +0,0 @@ -#ifndef crypto_core_salsa2012_H -#define crypto_core_salsa2012_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_core_salsa2012_OUTPUTBYTES 64U -SODIUM_EXPORT -size_t crypto_core_salsa2012_outputbytes(void); - -#define crypto_core_salsa2012_INPUTBYTES 16U -SODIUM_EXPORT -size_t crypto_core_salsa2012_inputbytes(void); - -#define crypto_core_salsa2012_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_core_salsa2012_keybytes(void); - -#define crypto_core_salsa2012_CONSTBYTES 16U -SODIUM_EXPORT -size_t crypto_core_salsa2012_constbytes(void); - -SODIUM_EXPORT -int crypto_core_salsa2012(unsigned char *out, const unsigned char *in, - const unsigned char *k, const unsigned char *c) - __attribute__ ((nonnull(1, 2, 3))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa208.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa208.h deleted file mode 100644 index d2f216af26..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_core_salsa208.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef crypto_core_salsa208_H -#define crypto_core_salsa208_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_core_salsa208_OUTPUTBYTES 64U -SODIUM_EXPORT -size_t crypto_core_salsa208_outputbytes(void) - __attribute__ ((deprecated)); - -#define crypto_core_salsa208_INPUTBYTES 16U -SODIUM_EXPORT -size_t crypto_core_salsa208_inputbytes(void) - __attribute__ ((deprecated)); - -#define crypto_core_salsa208_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_core_salsa208_keybytes(void) - __attribute__ ((deprecated)); - -#define crypto_core_salsa208_CONSTBYTES 16U -SODIUM_EXPORT -size_t crypto_core_salsa208_constbytes(void) - __attribute__ ((deprecated)); - -SODIUM_EXPORT -int crypto_core_salsa208(unsigned char *out, const unsigned char *in, - const unsigned char *k, const unsigned char *c) - __attribute__ ((nonnull(1, 2, 3))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash.h deleted file mode 100644 index d897e5d26c..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash.h +++ /dev/null @@ -1,84 +0,0 @@ -#ifndef crypto_generichash_H -#define crypto_generichash_H - -#include - -#include "crypto_generichash_blake2b.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_generichash_BYTES_MIN crypto_generichash_blake2b_BYTES_MIN -SODIUM_EXPORT -size_t crypto_generichash_bytes_min(void); - -#define crypto_generichash_BYTES_MAX crypto_generichash_blake2b_BYTES_MAX -SODIUM_EXPORT -size_t crypto_generichash_bytes_max(void); - -#define crypto_generichash_BYTES crypto_generichash_blake2b_BYTES -SODIUM_EXPORT -size_t crypto_generichash_bytes(void); - -#define crypto_generichash_KEYBYTES_MIN crypto_generichash_blake2b_KEYBYTES_MIN -SODIUM_EXPORT -size_t crypto_generichash_keybytes_min(void); - -#define crypto_generichash_KEYBYTES_MAX crypto_generichash_blake2b_KEYBYTES_MAX -SODIUM_EXPORT -size_t crypto_generichash_keybytes_max(void); - -#define crypto_generichash_KEYBYTES crypto_generichash_blake2b_KEYBYTES -SODIUM_EXPORT -size_t crypto_generichash_keybytes(void); - -#define crypto_generichash_PRIMITIVE "blake2b" -SODIUM_EXPORT -const char *crypto_generichash_primitive(void); - -/* - * Important when writing bindings for other programming languages: - * the state address should be 64-bytes aligned. - */ -typedef crypto_generichash_blake2b_state crypto_generichash_state; - -SODIUM_EXPORT -size_t crypto_generichash_statebytes(void); - -SODIUM_EXPORT -int crypto_generichash(unsigned char *out, size_t outlen, - const unsigned char *in, unsigned long long inlen, - const unsigned char *key, size_t keylen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_generichash_init(crypto_generichash_state *state, - const unsigned char *key, - const size_t keylen, const size_t outlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_generichash_update(crypto_generichash_state *state, - const unsigned char *in, - unsigned long long inlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_generichash_final(crypto_generichash_state *state, - unsigned char *out, const size_t outlen) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_generichash_keygen(unsigned char k[crypto_generichash_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash_blake2b.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash_blake2b.h deleted file mode 100644 index ae3b52f7de..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_generichash_blake2b.h +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef crypto_generichash_blake2b_H -#define crypto_generichash_blake2b_H - -#include -#include -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#ifdef __IBMC__ -# pragma pack(1) -#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) -# pragma pack(1) -#else -# pragma pack(push, 1) -#endif - -typedef struct CRYPTO_ALIGN(64) crypto_generichash_blake2b_state { - unsigned char opaque[384]; -} crypto_generichash_blake2b_state; - -#ifdef __IBMC__ -# pragma pack(pop) -#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC) -# pragma pack() -#else -# pragma pack(pop) -#endif - -#define crypto_generichash_blake2b_BYTES_MIN 16U -SODIUM_EXPORT -size_t crypto_generichash_blake2b_bytes_min(void); - -#define crypto_generichash_blake2b_BYTES_MAX 64U -SODIUM_EXPORT -size_t crypto_generichash_blake2b_bytes_max(void); - -#define crypto_generichash_blake2b_BYTES 32U -SODIUM_EXPORT -size_t crypto_generichash_blake2b_bytes(void); - -#define crypto_generichash_blake2b_KEYBYTES_MIN 16U -SODIUM_EXPORT -size_t crypto_generichash_blake2b_keybytes_min(void); - -#define crypto_generichash_blake2b_KEYBYTES_MAX 64U -SODIUM_EXPORT -size_t crypto_generichash_blake2b_keybytes_max(void); - -#define crypto_generichash_blake2b_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_generichash_blake2b_keybytes(void); - -#define crypto_generichash_blake2b_SALTBYTES 16U -SODIUM_EXPORT -size_t crypto_generichash_blake2b_saltbytes(void); - -#define crypto_generichash_blake2b_PERSONALBYTES 16U -SODIUM_EXPORT -size_t crypto_generichash_blake2b_personalbytes(void); - -SODIUM_EXPORT -size_t crypto_generichash_blake2b_statebytes(void); - -SODIUM_EXPORT -int crypto_generichash_blake2b(unsigned char *out, size_t outlen, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *key, size_t keylen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_generichash_blake2b_salt_personal(unsigned char *out, size_t outlen, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *key, - size_t keylen, - const unsigned char *salt, - const unsigned char *personal) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_generichash_blake2b_init(crypto_generichash_blake2b_state *state, - const unsigned char *key, - const size_t keylen, const size_t outlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_generichash_blake2b_init_salt_personal(crypto_generichash_blake2b_state *state, - const unsigned char *key, - const size_t keylen, const size_t outlen, - const unsigned char *salt, - const unsigned char *personal) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_generichash_blake2b_update(crypto_generichash_blake2b_state *state, - const unsigned char *in, - unsigned long long inlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_generichash_blake2b_final(crypto_generichash_blake2b_state *state, - unsigned char *out, - const size_t outlen) __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_generichash_blake2b_keygen(unsigned char k[crypto_generichash_blake2b_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash.h deleted file mode 100644 index 767d548018..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash.h +++ /dev/null @@ -1,40 +0,0 @@ -#ifndef crypto_hash_H -#define crypto_hash_H - -/* - * WARNING: Unless you absolutely need to use SHA512 for interoperability, - * purposes, you might want to consider crypto_generichash() instead. - * Unlike SHA512, crypto_generichash() is not vulnerable to length - * extension attacks. - */ - -#include - -#include "crypto_hash_sha512.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_hash_BYTES crypto_hash_sha512_BYTES -SODIUM_EXPORT -size_t crypto_hash_bytes(void); - -SODIUM_EXPORT -int crypto_hash(unsigned char *out, const unsigned char *in, - unsigned long long inlen) __attribute__ ((nonnull(1))); - -#define crypto_hash_PRIMITIVE "sha512" -SODIUM_EXPORT -const char *crypto_hash_primitive(void) - __attribute__ ((warn_unused_result)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha256.h deleted file mode 100644 index f8311b2e88..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha256.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef crypto_hash_sha256_H -#define crypto_hash_sha256_H - -/* - * WARNING: Unless you absolutely need to use SHA256 for interoperability - * purposes, you might want to consider crypto_generichash() instead. - * Unlike SHA256, crypto_generichash() is not vulnerable to length - * extension attacks. - */ - -#include -#include -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -typedef struct crypto_hash_sha256_state { - uint32_t state[8]; - uint64_t count; - uint8_t buf[64]; -} crypto_hash_sha256_state; - -SODIUM_EXPORT -size_t crypto_hash_sha256_statebytes(void); - -#define crypto_hash_sha256_BYTES 32U -SODIUM_EXPORT -size_t crypto_hash_sha256_bytes(void); - -SODIUM_EXPORT -int crypto_hash_sha256(unsigned char *out, const unsigned char *in, - unsigned long long inlen) __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_hash_sha256_init(crypto_hash_sha256_state *state) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_hash_sha256_update(crypto_hash_sha256_state *state, - const unsigned char *in, - unsigned long long inlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_hash_sha256_final(crypto_hash_sha256_state *state, - unsigned char *out) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha512.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha512.h deleted file mode 100644 index 1628901cf2..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_hash_sha512.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef crypto_hash_sha512_H -#define crypto_hash_sha512_H - -/* - * WARNING: Unless you absolutely need to use SHA512 for interoperability - * purposes, you might want to consider crypto_generichash() instead. - * Unlike SHA512, crypto_generichash() is not vulnerable to length - * extension attacks. - */ - -#include -#include -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -typedef struct crypto_hash_sha512_state { - uint64_t state[8]; - uint64_t count[2]; - uint8_t buf[128]; -} crypto_hash_sha512_state; - -SODIUM_EXPORT -size_t crypto_hash_sha512_statebytes(void); - -#define crypto_hash_sha512_BYTES 64U -SODIUM_EXPORT -size_t crypto_hash_sha512_bytes(void); - -SODIUM_EXPORT -int crypto_hash_sha512(unsigned char *out, const unsigned char *in, - unsigned long long inlen) __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_hash_sha512_init(crypto_hash_sha512_state *state) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_hash_sha512_update(crypto_hash_sha512_state *state, - const unsigned char *in, - unsigned long long inlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_hash_sha512_final(crypto_hash_sha512_state *state, - unsigned char *out) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_ipcrypt.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_ipcrypt.h deleted file mode 100644 index 65f526d2a6..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_ipcrypt.h +++ /dev/null @@ -1,132 +0,0 @@ -#ifndef crypto_ipcrypt_H -#define crypto_ipcrypt_H - -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_ipcrypt_BYTES 16U -SODIUM_EXPORT -size_t crypto_ipcrypt_bytes(void); - -#define crypto_ipcrypt_KEYBYTES 16U -SODIUM_EXPORT -size_t crypto_ipcrypt_keybytes(void); - -#define crypto_ipcrypt_ND_KEYBYTES 16U -SODIUM_EXPORT -size_t crypto_ipcrypt_nd_keybytes(void); - -#define crypto_ipcrypt_ND_TWEAKBYTES 8U -SODIUM_EXPORT -size_t crypto_ipcrypt_nd_tweakbytes(void); - -#define crypto_ipcrypt_ND_INPUTBYTES 16U -SODIUM_EXPORT -size_t crypto_ipcrypt_nd_inputbytes(void); - -#define crypto_ipcrypt_ND_OUTPUTBYTES 24U -SODIUM_EXPORT -size_t crypto_ipcrypt_nd_outputbytes(void); - -#define crypto_ipcrypt_NDX_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_ipcrypt_ndx_keybytes(void); - -#define crypto_ipcrypt_NDX_TWEAKBYTES 16U -SODIUM_EXPORT -size_t crypto_ipcrypt_ndx_tweakbytes(void); - -#define crypto_ipcrypt_NDX_INPUTBYTES 16U -SODIUM_EXPORT -size_t crypto_ipcrypt_ndx_inputbytes(void); - -#define crypto_ipcrypt_NDX_OUTPUTBYTES 32U -SODIUM_EXPORT -size_t crypto_ipcrypt_ndx_outputbytes(void); - -#define crypto_ipcrypt_PFX_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_ipcrypt_pfx_keybytes(void); - -#define crypto_ipcrypt_PFX_BYTES 16U -SODIUM_EXPORT -size_t crypto_ipcrypt_pfx_bytes(void); - -SODIUM_EXPORT -void crypto_ipcrypt_keygen(unsigned char k[crypto_ipcrypt_KEYBYTES]) __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_nd_keygen(unsigned char k[crypto_ipcrypt_ND_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_ndx_keygen(unsigned char k[crypto_ipcrypt_NDX_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_pfx_keygen(unsigned char k[crypto_ipcrypt_PFX_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_encrypt(unsigned char out[crypto_ipcrypt_BYTES], - const unsigned char in[crypto_ipcrypt_BYTES], - const unsigned char k[crypto_ipcrypt_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_decrypt(unsigned char out[crypto_ipcrypt_BYTES], - const unsigned char in[crypto_ipcrypt_BYTES], - const unsigned char k[crypto_ipcrypt_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_nd_encrypt(unsigned char out[crypto_ipcrypt_ND_OUTPUTBYTES], - const unsigned char in[crypto_ipcrypt_ND_INPUTBYTES], - const unsigned char t[crypto_ipcrypt_ND_TWEAKBYTES], - const unsigned char k[crypto_ipcrypt_ND_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_nd_decrypt(unsigned char out[crypto_ipcrypt_ND_INPUTBYTES], - const unsigned char in[crypto_ipcrypt_ND_OUTPUTBYTES], - const unsigned char k[crypto_ipcrypt_ND_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_ndx_encrypt(unsigned char out[crypto_ipcrypt_NDX_OUTPUTBYTES], - const unsigned char in[crypto_ipcrypt_NDX_INPUTBYTES], - const unsigned char t[crypto_ipcrypt_NDX_TWEAKBYTES], - const unsigned char k[crypto_ipcrypt_NDX_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_ndx_decrypt(unsigned char out[crypto_ipcrypt_NDX_INPUTBYTES], - const unsigned char in[crypto_ipcrypt_NDX_OUTPUTBYTES], - const unsigned char k[crypto_ipcrypt_NDX_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_pfx_encrypt(unsigned char out[crypto_ipcrypt_PFX_BYTES], - const unsigned char in[crypto_ipcrypt_PFX_BYTES], - const unsigned char k[crypto_ipcrypt_PFX_KEYBYTES]) - __attribute__((nonnull)); - -SODIUM_EXPORT -void crypto_ipcrypt_pfx_decrypt(unsigned char out[crypto_ipcrypt_PFX_BYTES], - const unsigned char in[crypto_ipcrypt_PFX_BYTES], - const unsigned char k[crypto_ipcrypt_PFX_KEYBYTES]) - __attribute__((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf.h deleted file mode 100644 index ac2fc6183c..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf.h +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef crypto_kdf_H -#define crypto_kdf_H - -#include -#include - -#include "crypto_kdf_blake2b.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_kdf_BYTES_MIN crypto_kdf_blake2b_BYTES_MIN -SODIUM_EXPORT -size_t crypto_kdf_bytes_min(void); - -#define crypto_kdf_BYTES_MAX crypto_kdf_blake2b_BYTES_MAX -SODIUM_EXPORT -size_t crypto_kdf_bytes_max(void); - -#define crypto_kdf_CONTEXTBYTES crypto_kdf_blake2b_CONTEXTBYTES -SODIUM_EXPORT -size_t crypto_kdf_contextbytes(void); - -#define crypto_kdf_KEYBYTES crypto_kdf_blake2b_KEYBYTES -SODIUM_EXPORT -size_t crypto_kdf_keybytes(void); - -#define crypto_kdf_PRIMITIVE "blake2b" -SODIUM_EXPORT -const char *crypto_kdf_primitive(void) - __attribute__ ((warn_unused_result)); - -SODIUM_EXPORT -int crypto_kdf_derive_from_key(unsigned char *subkey, size_t subkey_len, - uint64_t subkey_id, - const char ctx[crypto_kdf_CONTEXTBYTES], - const unsigned char key[crypto_kdf_KEYBYTES]) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_kdf_keygen(unsigned char k[crypto_kdf_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_blake2b.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_blake2b.h deleted file mode 100644 index 489c7c20fc..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_blake2b.h +++ /dev/null @@ -1,44 +0,0 @@ -#ifndef crypto_kdf_blake2b_H -#define crypto_kdf_blake2b_H - -#include -#include -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_kdf_blake2b_BYTES_MIN 16 -SODIUM_EXPORT -size_t crypto_kdf_blake2b_bytes_min(void); - -#define crypto_kdf_blake2b_BYTES_MAX 64 -SODIUM_EXPORT -size_t crypto_kdf_blake2b_bytes_max(void); - -#define crypto_kdf_blake2b_CONTEXTBYTES 8 -SODIUM_EXPORT -size_t crypto_kdf_blake2b_contextbytes(void); - -#define crypto_kdf_blake2b_KEYBYTES 32 -SODIUM_EXPORT -size_t crypto_kdf_blake2b_keybytes(void); - -SODIUM_EXPORT -int crypto_kdf_blake2b_derive_from_key(unsigned char *subkey, size_t subkey_len, - uint64_t subkey_id, - const char ctx[crypto_kdf_blake2b_CONTEXTBYTES], - const unsigned char key[crypto_kdf_blake2b_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha256.h deleted file mode 100644 index e7e7f4db6d..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha256.h +++ /dev/null @@ -1,74 +0,0 @@ -#ifndef crypto_kdf_hkdf_sha256_H -#define crypto_kdf_hkdf_sha256_H - -#include -#include -#include - -#include "crypto_kdf.h" -#include "crypto_auth_hmacsha256.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_kdf_hkdf_sha256_KEYBYTES crypto_auth_hmacsha256_BYTES -SODIUM_EXPORT -size_t crypto_kdf_hkdf_sha256_keybytes(void); - -#define crypto_kdf_hkdf_sha256_BYTES_MIN 0U -SODIUM_EXPORT -size_t crypto_kdf_hkdf_sha256_bytes_min(void); - -#define crypto_kdf_hkdf_sha256_BYTES_MAX (0xff * crypto_auth_hmacsha256_BYTES) -SODIUM_EXPORT -size_t crypto_kdf_hkdf_sha256_bytes_max(void); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha256_extract(unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES], - const unsigned char *salt, size_t salt_len, - const unsigned char *ikm, size_t ikm_len) - __attribute__ ((nonnull(4))); - -SODIUM_EXPORT -void crypto_kdf_hkdf_sha256_keygen(unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES]); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha256_expand(unsigned char *out, size_t out_len, - const char *ctx, size_t ctx_len, - const unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES]) - __attribute__ ((nonnull(1))); - -/* ------------------------------------------------------------------------- */ - -typedef struct crypto_kdf_hkdf_sha256_state { - crypto_auth_hmacsha256_state st; -} crypto_kdf_hkdf_sha256_state; - -SODIUM_EXPORT -size_t crypto_kdf_hkdf_sha256_statebytes(void); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha256_extract_init(crypto_kdf_hkdf_sha256_state *state, - const unsigned char *salt, size_t salt_len) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha256_extract_update(crypto_kdf_hkdf_sha256_state *state, - const unsigned char *ikm, size_t ikm_len) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha256_extract_final(crypto_kdf_hkdf_sha256_state *state, - unsigned char prk[crypto_kdf_hkdf_sha256_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha512.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha512.h deleted file mode 100644 index 0ed205dfb8..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kdf_hkdf_sha512.h +++ /dev/null @@ -1,75 +0,0 @@ -#ifndef crypto_kdf_hkdf_sha512_H -#define crypto_kdf_hkdf_sha512_H - -#include -#include -#include - -#include "crypto_kdf.h" -#include "crypto_auth_hmacsha512.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_kdf_hkdf_sha512_KEYBYTES crypto_auth_hmacsha512_BYTES -SODIUM_EXPORT -size_t crypto_kdf_hkdf_sha512_keybytes(void); - -#define crypto_kdf_hkdf_sha512_BYTES_MIN 0U -SODIUM_EXPORT -size_t crypto_kdf_hkdf_sha512_bytes_min(void); - -#define crypto_kdf_hkdf_sha512_BYTES_MAX (0xff * crypto_auth_hmacsha512_BYTES) -SODIUM_EXPORT -size_t crypto_kdf_hkdf_sha512_bytes_max(void); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha512_extract(unsigned char prk[crypto_kdf_hkdf_sha512_KEYBYTES], - const unsigned char *salt, size_t salt_len, - const unsigned char *ikm, size_t ikm_len) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -void crypto_kdf_hkdf_sha512_keygen(unsigned char prk[crypto_kdf_hkdf_sha512_KEYBYTES]) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha512_expand(unsigned char *out, size_t out_len, - const char *ctx, size_t ctx_len, - const unsigned char prk[crypto_kdf_hkdf_sha512_KEYBYTES]) - __attribute__ ((nonnull(1))); - -/* ------------------------------------------------------------------------- */ - -typedef struct crypto_kdf_hkdf_sha512_state { - crypto_auth_hmacsha512_state st; -} crypto_kdf_hkdf_sha512_state; - -SODIUM_EXPORT -size_t crypto_kdf_hkdf_sha512_statebytes(void); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha512_extract_init(crypto_kdf_hkdf_sha512_state *state, - const unsigned char *salt, size_t salt_len) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha512_extract_update(crypto_kdf_hkdf_sha512_state *state, - const unsigned char *ikm, size_t ikm_len) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_kdf_hkdf_sha512_extract_final(crypto_kdf_hkdf_sha512_state *state, - unsigned char prk[crypto_kdf_hkdf_sha512_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kx.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kx.h deleted file mode 100644 index 347132c320..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_kx.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef crypto_kx_H -#define crypto_kx_H - -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_kx_PUBLICKEYBYTES 32 -SODIUM_EXPORT -size_t crypto_kx_publickeybytes(void); - -#define crypto_kx_SECRETKEYBYTES 32 -SODIUM_EXPORT -size_t crypto_kx_secretkeybytes(void); - -#define crypto_kx_SEEDBYTES 32 -SODIUM_EXPORT -size_t crypto_kx_seedbytes(void); - -#define crypto_kx_SESSIONKEYBYTES 32 -SODIUM_EXPORT -size_t crypto_kx_sessionkeybytes(void); - -#define crypto_kx_PRIMITIVE "x25519blake2b" -SODIUM_EXPORT -const char *crypto_kx_primitive(void); - -SODIUM_EXPORT -int crypto_kx_seed_keypair(unsigned char pk[crypto_kx_PUBLICKEYBYTES], - unsigned char sk[crypto_kx_SECRETKEYBYTES], - const unsigned char seed[crypto_kx_SEEDBYTES]) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_kx_keypair(unsigned char pk[crypto_kx_PUBLICKEYBYTES], - unsigned char sk[crypto_kx_SECRETKEYBYTES]) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_kx_client_session_keys(unsigned char rx[crypto_kx_SESSIONKEYBYTES], - unsigned char tx[crypto_kx_SESSIONKEYBYTES], - const unsigned char client_pk[crypto_kx_PUBLICKEYBYTES], - const unsigned char client_sk[crypto_kx_SECRETKEYBYTES], - const unsigned char server_pk[crypto_kx_PUBLICKEYBYTES]) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 4, 5))); - -SODIUM_EXPORT -int crypto_kx_server_session_keys(unsigned char rx[crypto_kx_SESSIONKEYBYTES], - unsigned char tx[crypto_kx_SESSIONKEYBYTES], - const unsigned char server_pk[crypto_kx_PUBLICKEYBYTES], - const unsigned char server_sk[crypto_kx_SECRETKEYBYTES], - const unsigned char client_pk[crypto_kx_PUBLICKEYBYTES]) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 4, 5))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth.h deleted file mode 100644 index 7cd7b07060..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth.h +++ /dev/null @@ -1,65 +0,0 @@ -#ifndef crypto_onetimeauth_H -#define crypto_onetimeauth_H - -#include - -#include "crypto_onetimeauth_poly1305.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -typedef crypto_onetimeauth_poly1305_state crypto_onetimeauth_state; - -SODIUM_EXPORT -size_t crypto_onetimeauth_statebytes(void); - -#define crypto_onetimeauth_BYTES crypto_onetimeauth_poly1305_BYTES -SODIUM_EXPORT -size_t crypto_onetimeauth_bytes(void); - -#define crypto_onetimeauth_KEYBYTES crypto_onetimeauth_poly1305_KEYBYTES -SODIUM_EXPORT -size_t crypto_onetimeauth_keybytes(void); - -#define crypto_onetimeauth_PRIMITIVE "poly1305" -SODIUM_EXPORT -const char *crypto_onetimeauth_primitive(void); - -SODIUM_EXPORT -int crypto_onetimeauth(unsigned char *out, const unsigned char *in, - unsigned long long inlen, const unsigned char *k) - __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_onetimeauth_verify(const unsigned char *h, const unsigned char *in, - unsigned long long inlen, const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_onetimeauth_init(crypto_onetimeauth_state *state, - const unsigned char *key) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_onetimeauth_update(crypto_onetimeauth_state *state, - const unsigned char *in, - unsigned long long inlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_onetimeauth_final(crypto_onetimeauth_state *state, - unsigned char *out) __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_onetimeauth_keygen(unsigned char k[crypto_onetimeauth_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth_poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth_poly1305.h deleted file mode 100644 index 1565d37aba..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_onetimeauth_poly1305.h +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef crypto_onetimeauth_poly1305_H -#define crypto_onetimeauth_poly1305_H - -#include -#include -#include - -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -typedef struct CRYPTO_ALIGN(16) crypto_onetimeauth_poly1305_state { - unsigned char opaque[256]; -} crypto_onetimeauth_poly1305_state; - -SODIUM_EXPORT -size_t crypto_onetimeauth_poly1305_statebytes(void); - -#define crypto_onetimeauth_poly1305_BYTES 16U -SODIUM_EXPORT -size_t crypto_onetimeauth_poly1305_bytes(void); - -#define crypto_onetimeauth_poly1305_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_onetimeauth_poly1305_keybytes(void); - -SODIUM_EXPORT -int crypto_onetimeauth_poly1305(unsigned char *out, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *k) - __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_onetimeauth_poly1305_verify(const unsigned char *h, - const unsigned char *in, - unsigned long long inlen, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_onetimeauth_poly1305_init(crypto_onetimeauth_poly1305_state *state, - const unsigned char *key) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_onetimeauth_poly1305_update(crypto_onetimeauth_poly1305_state *state, - const unsigned char *in, - unsigned long long inlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_onetimeauth_poly1305_final(crypto_onetimeauth_poly1305_state *state, - unsigned char *out) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void crypto_onetimeauth_poly1305_keygen(unsigned char k[crypto_onetimeauth_poly1305_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash.h deleted file mode 100644 index 9683192bf5..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash.h +++ /dev/null @@ -1,147 +0,0 @@ -#ifndef crypto_pwhash_H -#define crypto_pwhash_H - -#include - -#include "crypto_pwhash_argon2i.h" -#include "crypto_pwhash_argon2id.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_pwhash_ALG_ARGON2I13 crypto_pwhash_argon2i_ALG_ARGON2I13 -SODIUM_EXPORT -int crypto_pwhash_alg_argon2i13(void); - -#define crypto_pwhash_ALG_ARGON2ID13 crypto_pwhash_argon2id_ALG_ARGON2ID13 -SODIUM_EXPORT -int crypto_pwhash_alg_argon2id13(void); - -#define crypto_pwhash_ALG_DEFAULT crypto_pwhash_ALG_ARGON2ID13 -SODIUM_EXPORT -int crypto_pwhash_alg_default(void); - -#define crypto_pwhash_BYTES_MIN crypto_pwhash_argon2id_BYTES_MIN -SODIUM_EXPORT -size_t crypto_pwhash_bytes_min(void); - -#define crypto_pwhash_BYTES_MAX crypto_pwhash_argon2id_BYTES_MAX -SODIUM_EXPORT -size_t crypto_pwhash_bytes_max(void); - -#define crypto_pwhash_PASSWD_MIN crypto_pwhash_argon2id_PASSWD_MIN -SODIUM_EXPORT -size_t crypto_pwhash_passwd_min(void); - -#define crypto_pwhash_PASSWD_MAX crypto_pwhash_argon2id_PASSWD_MAX -SODIUM_EXPORT -size_t crypto_pwhash_passwd_max(void); - -#define crypto_pwhash_SALTBYTES crypto_pwhash_argon2id_SALTBYTES -SODIUM_EXPORT -size_t crypto_pwhash_saltbytes(void); - -#define crypto_pwhash_STRBYTES crypto_pwhash_argon2id_STRBYTES -SODIUM_EXPORT -size_t crypto_pwhash_strbytes(void); - -#define crypto_pwhash_STRPREFIX crypto_pwhash_argon2id_STRPREFIX -SODIUM_EXPORT -const char *crypto_pwhash_strprefix(void); - -#define crypto_pwhash_OPSLIMIT_MIN crypto_pwhash_argon2id_OPSLIMIT_MIN -SODIUM_EXPORT -size_t crypto_pwhash_opslimit_min(void); - -#define crypto_pwhash_OPSLIMIT_MAX crypto_pwhash_argon2id_OPSLIMIT_MAX -SODIUM_EXPORT -size_t crypto_pwhash_opslimit_max(void); - -#define crypto_pwhash_MEMLIMIT_MIN crypto_pwhash_argon2id_MEMLIMIT_MIN -SODIUM_EXPORT -size_t crypto_pwhash_memlimit_min(void); - -#define crypto_pwhash_MEMLIMIT_MAX crypto_pwhash_argon2id_MEMLIMIT_MAX -SODIUM_EXPORT -size_t crypto_pwhash_memlimit_max(void); - -#define crypto_pwhash_OPSLIMIT_INTERACTIVE crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE -SODIUM_EXPORT -size_t crypto_pwhash_opslimit_interactive(void); - -#define crypto_pwhash_MEMLIMIT_INTERACTIVE crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE -SODIUM_EXPORT -size_t crypto_pwhash_memlimit_interactive(void); - -#define crypto_pwhash_OPSLIMIT_MODERATE crypto_pwhash_argon2id_OPSLIMIT_MODERATE -SODIUM_EXPORT -size_t crypto_pwhash_opslimit_moderate(void); - -#define crypto_pwhash_MEMLIMIT_MODERATE crypto_pwhash_argon2id_MEMLIMIT_MODERATE -SODIUM_EXPORT -size_t crypto_pwhash_memlimit_moderate(void); - -#define crypto_pwhash_OPSLIMIT_SENSITIVE crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE -SODIUM_EXPORT -size_t crypto_pwhash_opslimit_sensitive(void); - -#define crypto_pwhash_MEMLIMIT_SENSITIVE crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE -SODIUM_EXPORT -size_t crypto_pwhash_memlimit_sensitive(void); - -/* - * With this function, do not forget to store all parameters, including the - * algorithm identifier in order to produce deterministic output. - * The crypto_pwhash_* definitions, including crypto_pwhash_ALG_DEFAULT, - * may change. - */ -SODIUM_EXPORT -int crypto_pwhash(unsigned char * const out, unsigned long long outlen, - const char * const passwd, unsigned long long passwdlen, - const unsigned char * const salt, - unsigned long long opslimit, size_t memlimit, int alg) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -/* - * The output string already includes all the required parameters, including - * the algorithm identifier. The string is all that has to be stored in - * order to verify a password. - */ -SODIUM_EXPORT -int crypto_pwhash_str(char out[crypto_pwhash_STRBYTES], - const char * const passwd, unsigned long long passwdlen, - unsigned long long opslimit, size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_str_alg(char out[crypto_pwhash_STRBYTES], - const char * const passwd, unsigned long long passwdlen, - unsigned long long opslimit, size_t memlimit, int alg) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_str_verify(const char *str, - const char * const passwd, - unsigned long long passwdlen) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_str_needs_rehash(const char *str, - unsigned long long opslimit, size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#define crypto_pwhash_PRIMITIVE "argon2id,argon2i" -SODIUM_EXPORT -const char *crypto_pwhash_primitive(void) - __attribute__ ((warn_unused_result)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2i.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2i.h deleted file mode 100644 index 63abdf4ab9..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2i.h +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef crypto_pwhash_argon2i_H -#define crypto_pwhash_argon2i_H - -#include -#include -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_pwhash_argon2i_ALG_ARGON2I13 1 -SODIUM_EXPORT -int crypto_pwhash_argon2i_alg_argon2i13(void); - -#define crypto_pwhash_argon2i_BYTES_MIN 16U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_bytes_min(void); - -#define crypto_pwhash_argon2i_BYTES_MAX SODIUM_MIN(SODIUM_SIZE_MAX, 4294967295U) -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_bytes_max(void); - -#define crypto_pwhash_argon2i_PASSWD_MIN 0U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_passwd_min(void); - -#define crypto_pwhash_argon2i_PASSWD_MAX 4294967295U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_passwd_max(void); - -#define crypto_pwhash_argon2i_SALTBYTES 16U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_saltbytes(void); - -#define crypto_pwhash_argon2i_STRBYTES 128U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_strbytes(void); - -#define crypto_pwhash_argon2i_STRPREFIX "$argon2i$" -SODIUM_EXPORT -const char *crypto_pwhash_argon2i_strprefix(void); - -#define crypto_pwhash_argon2i_OPSLIMIT_MIN 3U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_opslimit_min(void); - -#define crypto_pwhash_argon2i_OPSLIMIT_MAX 4294967295U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_opslimit_max(void); - -#define crypto_pwhash_argon2i_MEMLIMIT_MIN 8192U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_memlimit_min(void); - -#define crypto_pwhash_argon2i_MEMLIMIT_MAX \ - ((SIZE_MAX >= 4398046510080U) ? 4398046510080U : (SIZE_MAX >= 2147483648U) ? 2147483648U : 32768U) -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_memlimit_max(void); - -#define crypto_pwhash_argon2i_OPSLIMIT_INTERACTIVE 4U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_opslimit_interactive(void); - -#define crypto_pwhash_argon2i_MEMLIMIT_INTERACTIVE 33554432U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_memlimit_interactive(void); - -#define crypto_pwhash_argon2i_OPSLIMIT_MODERATE 6U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_opslimit_moderate(void); - -#define crypto_pwhash_argon2i_MEMLIMIT_MODERATE 134217728U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_memlimit_moderate(void); - -#define crypto_pwhash_argon2i_OPSLIMIT_SENSITIVE 8U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_opslimit_sensitive(void); - -#define crypto_pwhash_argon2i_MEMLIMIT_SENSITIVE 536870912U -SODIUM_EXPORT -size_t crypto_pwhash_argon2i_memlimit_sensitive(void); - -SODIUM_EXPORT -int crypto_pwhash_argon2i(unsigned char * const out, - unsigned long long outlen, - const char * const passwd, - unsigned long long passwdlen, - const unsigned char * const salt, - unsigned long long opslimit, size_t memlimit, - int alg) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_argon2i_str(char out[crypto_pwhash_argon2i_STRBYTES], - const char * const passwd, - unsigned long long passwdlen, - unsigned long long opslimit, size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_argon2i_str_verify(const char * str, - const char * const passwd, - unsigned long long passwdlen) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_argon2i_str_needs_rehash(const char * str, - unsigned long long opslimit, size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2id.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2id.h deleted file mode 100644 index 98bc83e075..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_argon2id.h +++ /dev/null @@ -1,122 +0,0 @@ -#ifndef crypto_pwhash_argon2id_H -#define crypto_pwhash_argon2id_H - -#include -#include -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_pwhash_argon2id_ALG_ARGON2ID13 2 -SODIUM_EXPORT -int crypto_pwhash_argon2id_alg_argon2id13(void); - -#define crypto_pwhash_argon2id_BYTES_MIN 16U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_bytes_min(void); - -#define crypto_pwhash_argon2id_BYTES_MAX SODIUM_MIN(SODIUM_SIZE_MAX, 4294967295U) -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_bytes_max(void); - -#define crypto_pwhash_argon2id_PASSWD_MIN 0U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_passwd_min(void); - -#define crypto_pwhash_argon2id_PASSWD_MAX 4294967295U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_passwd_max(void); - -#define crypto_pwhash_argon2id_SALTBYTES 16U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_saltbytes(void); - -#define crypto_pwhash_argon2id_STRBYTES 128U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_strbytes(void); - -#define crypto_pwhash_argon2id_STRPREFIX "$argon2id$" -SODIUM_EXPORT -const char *crypto_pwhash_argon2id_strprefix(void); - -#define crypto_pwhash_argon2id_OPSLIMIT_MIN 1U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_opslimit_min(void); - -#define crypto_pwhash_argon2id_OPSLIMIT_MAX 4294967295U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_opslimit_max(void); - -#define crypto_pwhash_argon2id_MEMLIMIT_MIN 8192U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_memlimit_min(void); - -#define crypto_pwhash_argon2id_MEMLIMIT_MAX \ - ((SIZE_MAX >= 4398046510080U) ? 4398046510080U : (SIZE_MAX >= 2147483648U) ? 2147483648U : 32768U) -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_memlimit_max(void); - -#define crypto_pwhash_argon2id_OPSLIMIT_INTERACTIVE 2U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_opslimit_interactive(void); - -#define crypto_pwhash_argon2id_MEMLIMIT_INTERACTIVE 67108864U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_memlimit_interactive(void); - -#define crypto_pwhash_argon2id_OPSLIMIT_MODERATE 3U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_opslimit_moderate(void); - -#define crypto_pwhash_argon2id_MEMLIMIT_MODERATE 268435456U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_memlimit_moderate(void); - -#define crypto_pwhash_argon2id_OPSLIMIT_SENSITIVE 4U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_opslimit_sensitive(void); - -#define crypto_pwhash_argon2id_MEMLIMIT_SENSITIVE 1073741824U -SODIUM_EXPORT -size_t crypto_pwhash_argon2id_memlimit_sensitive(void); - -SODIUM_EXPORT -int crypto_pwhash_argon2id(unsigned char * const out, - unsigned long long outlen, - const char * const passwd, - unsigned long long passwdlen, - const unsigned char * const salt, - unsigned long long opslimit, size_t memlimit, - int alg) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_argon2id_str(char out[crypto_pwhash_argon2id_STRBYTES], - const char * const passwd, - unsigned long long passwdlen, - unsigned long long opslimit, size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_argon2id_str_verify(const char * str, - const char * const passwd, - unsigned long long passwdlen) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_argon2id_str_needs_rehash(const char * str, - unsigned long long opslimit, size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_scryptsalsa208sha256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_scryptsalsa208sha256.h deleted file mode 100644 index 8956dfbf74..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_pwhash_scryptsalsa208sha256.h +++ /dev/null @@ -1,120 +0,0 @@ -#ifndef crypto_pwhash_scryptsalsa208sha256_H -#define crypto_pwhash_scryptsalsa208sha256_H - -#include -#include -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_pwhash_scryptsalsa208sha256_BYTES_MIN 16U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_bytes_min(void); - -#define crypto_pwhash_scryptsalsa208sha256_BYTES_MAX \ - SODIUM_MIN(SODIUM_SIZE_MAX, 0x1fffffffe0ULL) -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_bytes_max(void); - -#define crypto_pwhash_scryptsalsa208sha256_PASSWD_MIN 0U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_passwd_min(void); - -#define crypto_pwhash_scryptsalsa208sha256_PASSWD_MAX SODIUM_SIZE_MAX -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_passwd_max(void); - -#define crypto_pwhash_scryptsalsa208sha256_SALTBYTES 32U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_saltbytes(void); - -#define crypto_pwhash_scryptsalsa208sha256_STRBYTES 102U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_strbytes(void); - -#define crypto_pwhash_scryptsalsa208sha256_STRPREFIX "$7$" -SODIUM_EXPORT -const char *crypto_pwhash_scryptsalsa208sha256_strprefix(void); - -#define crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_MIN 32768U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_opslimit_min(void); - -#define crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_MAX 4294967295U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_opslimit_max(void); - -#define crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_MIN 16777216U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_memlimit_min(void); - -#define crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_MAX \ - SODIUM_MIN(SIZE_MAX, 68719476736ULL) -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_memlimit_max(void); - -#define crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE 524288U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_opslimit_interactive(void); - -#define crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE 16777216U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_memlimit_interactive(void); - -#define crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_SENSITIVE 33554432U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_opslimit_sensitive(void); - -#define crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_SENSITIVE 1073741824U -SODIUM_EXPORT -size_t crypto_pwhash_scryptsalsa208sha256_memlimit_sensitive(void); - -SODIUM_EXPORT -int crypto_pwhash_scryptsalsa208sha256(unsigned char * const out, - unsigned long long outlen, - const char * const passwd, - unsigned long long passwdlen, - const unsigned char * const salt, - unsigned long long opslimit, - size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_scryptsalsa208sha256_str(char out[crypto_pwhash_scryptsalsa208sha256_STRBYTES], - const char * const passwd, - unsigned long long passwdlen, - unsigned long long opslimit, - size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_scryptsalsa208sha256_str_verify(const char * str, - const char * const passwd, - unsigned long long passwdlen) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_scryptsalsa208sha256_ll(const uint8_t * passwd, size_t passwdlen, - const uint8_t * salt, size_t saltlen, - uint64_t N, uint32_t r, uint32_t p, - uint8_t * buf, size_t buflen) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_pwhash_scryptsalsa208sha256_str_needs_rehash(const char * str, - unsigned long long opslimit, - size_t memlimit) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult.h deleted file mode 100644 index 1c68585378..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef crypto_scalarmult_H -#define crypto_scalarmult_H - -#include - -#include "crypto_scalarmult_curve25519.h" -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_scalarmult_BYTES crypto_scalarmult_curve25519_BYTES -SODIUM_EXPORT -size_t crypto_scalarmult_bytes(void); - -#define crypto_scalarmult_SCALARBYTES crypto_scalarmult_curve25519_SCALARBYTES -SODIUM_EXPORT -size_t crypto_scalarmult_scalarbytes(void); - -#define crypto_scalarmult_PRIMITIVE "curve25519" -SODIUM_EXPORT -const char *crypto_scalarmult_primitive(void); - -SODIUM_EXPORT -int crypto_scalarmult_base(unsigned char *q, const unsigned char *n) - __attribute__ ((nonnull)); - -/* - * NOTE: Do not use the result of this function directly for key exchange. - * - * Hash the result with the public keys in order to compute a shared - * secret key: H(q || client_pk || server_pk) - * - * Or unless this is not an option, use the crypto_kx() API instead. - */ -SODIUM_EXPORT -int crypto_scalarmult(unsigned char *q, const unsigned char *n, - const unsigned char *p) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_curve25519.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_curve25519.h deleted file mode 100644 index 60e9d0c5a4..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_curve25519.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef crypto_scalarmult_curve25519_H -#define crypto_scalarmult_curve25519_H - -#include - -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_scalarmult_curve25519_BYTES 32U -SODIUM_EXPORT -size_t crypto_scalarmult_curve25519_bytes(void); - -#define crypto_scalarmult_curve25519_SCALARBYTES 32U -SODIUM_EXPORT -size_t crypto_scalarmult_curve25519_scalarbytes(void); - -/* - * NOTE: Do not use the result of this function directly for key exchange. - * - * Hash the result with the public keys in order to compute a shared - * secret key: H(q || client_pk || server_pk) - * - * Or unless this is not an option, use the crypto_kx() API instead. - */ -SODIUM_EXPORT -int crypto_scalarmult_curve25519(unsigned char *q, const unsigned char *n, - const unsigned char *p) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_scalarmult_curve25519_base(unsigned char *q, - const unsigned char *n) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ed25519.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ed25519.h deleted file mode 100644 index 2dfa4d7073..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ed25519.h +++ /dev/null @@ -1,51 +0,0 @@ - -#ifndef crypto_scalarmult_ed25519_H -#define crypto_scalarmult_ed25519_H - -#include - -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_scalarmult_ed25519_BYTES 32U -SODIUM_EXPORT -size_t crypto_scalarmult_ed25519_bytes(void); - -#define crypto_scalarmult_ed25519_SCALARBYTES 32U -SODIUM_EXPORT -size_t crypto_scalarmult_ed25519_scalarbytes(void); - -/* - * NOTE: Do not use the result of this function directly for key exchange. - * - * Hash the result with the public keys in order to compute a shared - * secret key: H(q || client_pk || server_pk) - * - * Or unless this is not an option, use the crypto_kx() API instead. - */ -SODIUM_EXPORT -int crypto_scalarmult_ed25519(unsigned char *q, const unsigned char *n, - const unsigned char *p) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_scalarmult_ed25519_noclamp(unsigned char *q, const unsigned char *n, - const unsigned char *p) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_scalarmult_ed25519_base(unsigned char *q, const unsigned char *n) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_scalarmult_ed25519_base_noclamp(unsigned char *q, const unsigned char *n) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ristretto255.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ristretto255.h deleted file mode 100644 index 40a45ccef0..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_scalarmult_ristretto255.h +++ /dev/null @@ -1,43 +0,0 @@ - -#ifndef crypto_scalarmult_ristretto255_H -#define crypto_scalarmult_ristretto255_H - -#include - -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_scalarmult_ristretto255_BYTES 32U -SODIUM_EXPORT -size_t crypto_scalarmult_ristretto255_bytes(void); - -#define crypto_scalarmult_ristretto255_SCALARBYTES 32U -SODIUM_EXPORT -size_t crypto_scalarmult_ristretto255_scalarbytes(void); - -/* - * NOTE: Do not use the result of this function directly for key exchange. - * - * Hash the result with the public keys in order to compute a shared - * secret key: H(q || client_pk || server_pk) - * - * Or unless this is not an option, use the crypto_kx() API instead. - */ -SODIUM_EXPORT -int crypto_scalarmult_ristretto255(unsigned char *q, const unsigned char *n, - const unsigned char *p) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_scalarmult_ristretto255_base(unsigned char *q, - const unsigned char *n) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox.h deleted file mode 100644 index 1d3709db12..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox.h +++ /dev/null @@ -1,93 +0,0 @@ -#ifndef crypto_secretbox_H -#define crypto_secretbox_H - -#include - -#include "crypto_secretbox_xsalsa20poly1305.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_secretbox_KEYBYTES crypto_secretbox_xsalsa20poly1305_KEYBYTES -SODIUM_EXPORT -size_t crypto_secretbox_keybytes(void); - -#define crypto_secretbox_NONCEBYTES crypto_secretbox_xsalsa20poly1305_NONCEBYTES -SODIUM_EXPORT -size_t crypto_secretbox_noncebytes(void); - -#define crypto_secretbox_MACBYTES crypto_secretbox_xsalsa20poly1305_MACBYTES -SODIUM_EXPORT -size_t crypto_secretbox_macbytes(void); - -#define crypto_secretbox_PRIMITIVE "xsalsa20poly1305" -SODIUM_EXPORT -const char *crypto_secretbox_primitive(void); - -#define crypto_secretbox_MESSAGEBYTES_MAX crypto_secretbox_xsalsa20poly1305_MESSAGEBYTES_MAX -SODIUM_EXPORT -size_t crypto_secretbox_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_secretbox_easy(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_secretbox_open_easy(unsigned char *m, const unsigned char *c, - unsigned long long clen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -SODIUM_EXPORT -int crypto_secretbox_detached(unsigned char *c, unsigned char *mac, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 2, 5, 6))); - -SODIUM_EXPORT -int crypto_secretbox_open_detached(unsigned char *m, - const unsigned char *c, - const unsigned char *mac, - unsigned long long clen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6))); - -SODIUM_EXPORT -void crypto_secretbox_keygen(unsigned char k[crypto_secretbox_KEYBYTES]) - __attribute__ ((nonnull)); - -/* -- NaCl compatibility interface ; Requires padding -- */ - -#define crypto_secretbox_ZEROBYTES crypto_secretbox_xsalsa20poly1305_ZEROBYTES -SODIUM_EXPORT -size_t crypto_secretbox_zerobytes(void); - -#define crypto_secretbox_BOXZEROBYTES crypto_secretbox_xsalsa20poly1305_BOXZEROBYTES -SODIUM_EXPORT -size_t crypto_secretbox_boxzerobytes(void); - -SODIUM_EXPORT -int crypto_secretbox(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_secretbox_open(unsigned char *m, const unsigned char *c, - unsigned long long clen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xchacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xchacha20poly1305.h deleted file mode 100644 index 6ec674e310..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xchacha20poly1305.h +++ /dev/null @@ -1,70 +0,0 @@ -#ifndef crypto_secretbox_xchacha20poly1305_H -#define crypto_secretbox_xchacha20poly1305_H - -#include -#include "crypto_stream_xchacha20.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_secretbox_xchacha20poly1305_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_secretbox_xchacha20poly1305_keybytes(void); - -#define crypto_secretbox_xchacha20poly1305_NONCEBYTES 24U -SODIUM_EXPORT -size_t crypto_secretbox_xchacha20poly1305_noncebytes(void); - -#define crypto_secretbox_xchacha20poly1305_MACBYTES 16U -SODIUM_EXPORT -size_t crypto_secretbox_xchacha20poly1305_macbytes(void); - -#define crypto_secretbox_xchacha20poly1305_MESSAGEBYTES_MAX \ - (crypto_stream_xchacha20_MESSAGEBYTES_MAX - crypto_secretbox_xchacha20poly1305_MACBYTES) -SODIUM_EXPORT -size_t crypto_secretbox_xchacha20poly1305_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_secretbox_xchacha20poly1305_easy(unsigned char *c, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_secretbox_xchacha20poly1305_open_easy(unsigned char *m, - const unsigned char *c, - unsigned long long clen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -SODIUM_EXPORT -int crypto_secretbox_xchacha20poly1305_detached(unsigned char *c, - unsigned char *mac, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 2, 5, 6))); - -SODIUM_EXPORT -int crypto_secretbox_xchacha20poly1305_open_detached(unsigned char *m, - const unsigned char *c, - const unsigned char *mac, - unsigned long long clen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 3, 5, 6))); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xsalsa20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xsalsa20poly1305.h deleted file mode 100644 index be0874cbaf..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretbox_xsalsa20poly1305.h +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef crypto_secretbox_xsalsa20poly1305_H -#define crypto_secretbox_xsalsa20poly1305_H - -#include -#include "crypto_stream_xsalsa20.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_secretbox_xsalsa20poly1305_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_secretbox_xsalsa20poly1305_keybytes(void); - -#define crypto_secretbox_xsalsa20poly1305_NONCEBYTES 24U -SODIUM_EXPORT -size_t crypto_secretbox_xsalsa20poly1305_noncebytes(void); - -#define crypto_secretbox_xsalsa20poly1305_MACBYTES 16U -SODIUM_EXPORT -size_t crypto_secretbox_xsalsa20poly1305_macbytes(void); - -/* Only for the libsodium API - The NaCl compatibility API would require BOXZEROBYTES extra bytes */ -#define crypto_secretbox_xsalsa20poly1305_MESSAGEBYTES_MAX \ - (crypto_stream_xsalsa20_MESSAGEBYTES_MAX - crypto_secretbox_xsalsa20poly1305_MACBYTES) -SODIUM_EXPORT -size_t crypto_secretbox_xsalsa20poly1305_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_secretbox_xsalsa20poly1305(unsigned char *c, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_secretbox_xsalsa20poly1305_open(unsigned char *m, - const unsigned char *c, - unsigned long long clen, - const unsigned char *n, - const unsigned char *k) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(2, 4, 5))); - -SODIUM_EXPORT -void crypto_secretbox_xsalsa20poly1305_keygen(unsigned char k[crypto_secretbox_xsalsa20poly1305_KEYBYTES]) - __attribute__ ((nonnull)); - -/* -- NaCl compatibility interface ; Requires padding -- */ - -#define crypto_secretbox_xsalsa20poly1305_BOXZEROBYTES 16U -SODIUM_EXPORT -size_t crypto_secretbox_xsalsa20poly1305_boxzerobytes(void); - -#define crypto_secretbox_xsalsa20poly1305_ZEROBYTES \ - (crypto_secretbox_xsalsa20poly1305_BOXZEROBYTES + \ - crypto_secretbox_xsalsa20poly1305_MACBYTES) -SODIUM_EXPORT -size_t crypto_secretbox_xsalsa20poly1305_zerobytes(void); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretstream_xchacha20poly1305.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretstream_xchacha20poly1305.h deleted file mode 100644 index b22e4e9313..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_secretstream_xchacha20poly1305.h +++ /dev/null @@ -1,108 +0,0 @@ -#ifndef crypto_secretstream_xchacha20poly1305_H -#define crypto_secretstream_xchacha20poly1305_H - -#include - -#include "crypto_aead_xchacha20poly1305.h" -#include "crypto_stream_chacha20.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_secretstream_xchacha20poly1305_ABYTES \ - (1U + crypto_aead_xchacha20poly1305_ietf_ABYTES) -SODIUM_EXPORT -size_t crypto_secretstream_xchacha20poly1305_abytes(void); - -#define crypto_secretstream_xchacha20poly1305_HEADERBYTES \ - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES -SODIUM_EXPORT -size_t crypto_secretstream_xchacha20poly1305_headerbytes(void); - -#define crypto_secretstream_xchacha20poly1305_KEYBYTES \ - crypto_aead_xchacha20poly1305_ietf_KEYBYTES -SODIUM_EXPORT -size_t crypto_secretstream_xchacha20poly1305_keybytes(void); - -#define crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX \ - SODIUM_MIN(SODIUM_SIZE_MAX - crypto_secretstream_xchacha20poly1305_ABYTES, \ - (64ULL * ((1ULL << 32) - 2ULL))) -SODIUM_EXPORT -size_t crypto_secretstream_xchacha20poly1305_messagebytes_max(void); - -#define crypto_secretstream_xchacha20poly1305_TAG_MESSAGE 0x00 -SODIUM_EXPORT -unsigned char crypto_secretstream_xchacha20poly1305_tag_message(void); - -#define crypto_secretstream_xchacha20poly1305_TAG_PUSH 0x01 -SODIUM_EXPORT -unsigned char crypto_secretstream_xchacha20poly1305_tag_push(void); - -#define crypto_secretstream_xchacha20poly1305_TAG_REKEY 0x02 -SODIUM_EXPORT -unsigned char crypto_secretstream_xchacha20poly1305_tag_rekey(void); - -#define crypto_secretstream_xchacha20poly1305_TAG_FINAL \ - (crypto_secretstream_xchacha20poly1305_TAG_PUSH | \ - crypto_secretstream_xchacha20poly1305_TAG_REKEY) -SODIUM_EXPORT -unsigned char crypto_secretstream_xchacha20poly1305_tag_final(void); - -typedef struct crypto_secretstream_xchacha20poly1305_state { - unsigned char k[crypto_stream_chacha20_ietf_KEYBYTES]; - unsigned char nonce[crypto_stream_chacha20_ietf_NONCEBYTES]; - unsigned char _pad[8]; -} crypto_secretstream_xchacha20poly1305_state; - -SODIUM_EXPORT -size_t crypto_secretstream_xchacha20poly1305_statebytes(void); - -SODIUM_EXPORT -void crypto_secretstream_xchacha20poly1305_keygen - (unsigned char k[crypto_secretstream_xchacha20poly1305_KEYBYTES]) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_secretstream_xchacha20poly1305_init_push - (crypto_secretstream_xchacha20poly1305_state *state, - unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES], - const unsigned char k[crypto_secretstream_xchacha20poly1305_KEYBYTES]) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_secretstream_xchacha20poly1305_push - (crypto_secretstream_xchacha20poly1305_state *state, - unsigned char *c, unsigned long long *clen_p, - const unsigned char *m, unsigned long long mlen, - const unsigned char *ad, unsigned long long adlen, unsigned char tag) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_secretstream_xchacha20poly1305_init_pull - (crypto_secretstream_xchacha20poly1305_state *state, - const unsigned char header[crypto_secretstream_xchacha20poly1305_HEADERBYTES], - const unsigned char k[crypto_secretstream_xchacha20poly1305_KEYBYTES]) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_secretstream_xchacha20poly1305_pull - (crypto_secretstream_xchacha20poly1305_state *state, - unsigned char *m, unsigned long long *mlen_p, unsigned char *tag_p, - const unsigned char *c, unsigned long long clen, - const unsigned char *ad, unsigned long long adlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -void crypto_secretstream_xchacha20poly1305_rekey - (crypto_secretstream_xchacha20poly1305_state *state); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash.h deleted file mode 100644 index fecaa88bd8..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef crypto_shorthash_H -#define crypto_shorthash_H - -#include - -#include "crypto_shorthash_siphash24.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_shorthash_BYTES crypto_shorthash_siphash24_BYTES -SODIUM_EXPORT -size_t crypto_shorthash_bytes(void); - -#define crypto_shorthash_KEYBYTES crypto_shorthash_siphash24_KEYBYTES -SODIUM_EXPORT -size_t crypto_shorthash_keybytes(void); - -#define crypto_shorthash_PRIMITIVE "siphash24" -SODIUM_EXPORT -const char *crypto_shorthash_primitive(void); - -SODIUM_EXPORT -int crypto_shorthash(unsigned char *out, const unsigned char *in, - unsigned long long inlen, const unsigned char *k) - __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -void crypto_shorthash_keygen(unsigned char k[crypto_shorthash_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash_siphash24.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash_siphash24.h deleted file mode 100644 index 1e6f72a620..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_shorthash_siphash24.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef crypto_shorthash_siphash24_H -#define crypto_shorthash_siphash24_H - -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -/* -- 64-bit output -- */ - -#define crypto_shorthash_siphash24_BYTES 8U -SODIUM_EXPORT -size_t crypto_shorthash_siphash24_bytes(void); - -#define crypto_shorthash_siphash24_KEYBYTES 16U -SODIUM_EXPORT -size_t crypto_shorthash_siphash24_keybytes(void); - -SODIUM_EXPORT -int crypto_shorthash_siphash24(unsigned char *out, const unsigned char *in, - unsigned long long inlen, const unsigned char *k) - __attribute__ ((nonnull(1, 4))); - -#ifndef SODIUM_LIBRARY_MINIMAL -/* -- 128-bit output -- */ - -#define crypto_shorthash_siphashx24_BYTES 16U -SODIUM_EXPORT -size_t crypto_shorthash_siphashx24_bytes(void); - -#define crypto_shorthash_siphashx24_KEYBYTES 16U -SODIUM_EXPORT -size_t crypto_shorthash_siphashx24_keybytes(void); - -SODIUM_EXPORT -int crypto_shorthash_siphashx24(unsigned char *out, const unsigned char *in, - unsigned long long inlen, const unsigned char *k) - __attribute__ ((nonnull(1, 4))); -#endif - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign.h deleted file mode 100644 index f5fafb123e..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign.h +++ /dev/null @@ -1,107 +0,0 @@ -#ifndef crypto_sign_H -#define crypto_sign_H - -/* - * THREAD SAFETY: crypto_sign_keypair() is thread-safe, - * provided that sodium_init() was called before. - * - * Other functions, including crypto_sign_seed_keypair() are always thread-safe. - */ - -#include - -#include "crypto_sign_ed25519.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -typedef crypto_sign_ed25519ph_state crypto_sign_state; - -SODIUM_EXPORT -size_t crypto_sign_statebytes(void); - -#define crypto_sign_BYTES crypto_sign_ed25519_BYTES -SODIUM_EXPORT -size_t crypto_sign_bytes(void); - -#define crypto_sign_SEEDBYTES crypto_sign_ed25519_SEEDBYTES -SODIUM_EXPORT -size_t crypto_sign_seedbytes(void); - -#define crypto_sign_PUBLICKEYBYTES crypto_sign_ed25519_PUBLICKEYBYTES -SODIUM_EXPORT -size_t crypto_sign_publickeybytes(void); - -#define crypto_sign_SECRETKEYBYTES crypto_sign_ed25519_SECRETKEYBYTES -SODIUM_EXPORT -size_t crypto_sign_secretkeybytes(void); - -#define crypto_sign_MESSAGEBYTES_MAX crypto_sign_ed25519_MESSAGEBYTES_MAX -SODIUM_EXPORT -size_t crypto_sign_messagebytes_max(void); - -#define crypto_sign_PRIMITIVE "ed25519" -SODIUM_EXPORT -const char *crypto_sign_primitive(void); - -SODIUM_EXPORT -int crypto_sign_seed_keypair(unsigned char *pk, unsigned char *sk, - const unsigned char *seed) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign_keypair(unsigned char *pk, unsigned char *sk) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign(unsigned char *sm, unsigned long long *smlen_p, - const unsigned char *m, unsigned long long mlen, - const unsigned char *sk) __attribute__ ((nonnull(1, 5))); - -SODIUM_EXPORT -int crypto_sign_open(unsigned char *m, unsigned long long *mlen_p, - const unsigned char *sm, unsigned long long smlen, - const unsigned char *pk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5))); - -SODIUM_EXPORT -int crypto_sign_detached(unsigned char *sig, unsigned long long *siglen_p, - const unsigned char *m, unsigned long long mlen, - const unsigned char *sk) __attribute__ ((nonnull(1, 5))); - -SODIUM_EXPORT -int crypto_sign_verify_detached(const unsigned char *sig, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *pk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_sign_init(crypto_sign_state *state); - -SODIUM_EXPORT -int crypto_sign_update(crypto_sign_state *state, - const unsigned char *m, unsigned long long mlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_sign_final_create(crypto_sign_state *state, unsigned char *sig, - unsigned long long *siglen_p, - const unsigned char *sk) - __attribute__ ((nonnull(1, 2, 4))); - -SODIUM_EXPORT -int crypto_sign_final_verify(crypto_sign_state *state, const unsigned char *sig, - const unsigned char *pk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_ed25519.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_ed25519.h deleted file mode 100644 index 0fdac42d35..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_ed25519.h +++ /dev/null @@ -1,124 +0,0 @@ -#ifndef crypto_sign_ed25519_H -#define crypto_sign_ed25519_H - -#include -#include "crypto_hash_sha512.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -typedef struct crypto_sign_ed25519ph_state { - crypto_hash_sha512_state hs; -} crypto_sign_ed25519ph_state; - -SODIUM_EXPORT -size_t crypto_sign_ed25519ph_statebytes(void); - -#define crypto_sign_ed25519_BYTES 64U -SODIUM_EXPORT -size_t crypto_sign_ed25519_bytes(void); - -#define crypto_sign_ed25519_SEEDBYTES 32U -SODIUM_EXPORT -size_t crypto_sign_ed25519_seedbytes(void); - -#define crypto_sign_ed25519_PUBLICKEYBYTES 32U -SODIUM_EXPORT -size_t crypto_sign_ed25519_publickeybytes(void); - -#define crypto_sign_ed25519_SECRETKEYBYTES (32U + 32U) -SODIUM_EXPORT -size_t crypto_sign_ed25519_secretkeybytes(void); - -#define crypto_sign_ed25519_MESSAGEBYTES_MAX (SODIUM_SIZE_MAX - crypto_sign_ed25519_BYTES) -SODIUM_EXPORT -size_t crypto_sign_ed25519_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_sign_ed25519(unsigned char *sm, unsigned long long *smlen_p, - const unsigned char *m, unsigned long long mlen, - const unsigned char *sk) - __attribute__ ((nonnull(1, 5))); - -SODIUM_EXPORT -int crypto_sign_ed25519_open(unsigned char *m, unsigned long long *mlen_p, - const unsigned char *sm, unsigned long long smlen, - const unsigned char *pk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(3, 5))); - -SODIUM_EXPORT -int crypto_sign_ed25519_detached(unsigned char *sig, - unsigned long long *siglen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *sk) - __attribute__ ((nonnull(1, 5))); - -SODIUM_EXPORT -int crypto_sign_ed25519_verify_detached(const unsigned char *sig, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *pk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull(1, 4))); - -SODIUM_EXPORT -int crypto_sign_ed25519_keypair(unsigned char *pk, unsigned char *sk) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign_ed25519_seed_keypair(unsigned char *pk, unsigned char *sk, - const unsigned char *seed) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign_ed25519_pk_to_curve25519(unsigned char *curve25519_pk, - const unsigned char *ed25519_pk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign_ed25519_sk_to_curve25519(unsigned char *curve25519_sk, - const unsigned char *ed25519_sk) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign_ed25519_sk_to_seed(unsigned char *seed, - const unsigned char *sk) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign_ed25519_sk_to_pk(unsigned char *pk, const unsigned char *sk) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign_ed25519ph_init(crypto_sign_ed25519ph_state *state) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_sign_ed25519ph_update(crypto_sign_ed25519ph_state *state, - const unsigned char *m, - unsigned long long mlen) - __attribute__ ((nonnull(1))); - -SODIUM_EXPORT -int crypto_sign_ed25519ph_final_create(crypto_sign_ed25519ph_state *state, - unsigned char *sig, - unsigned long long *siglen_p, - const unsigned char *sk) - __attribute__ ((nonnull(1, 2, 4))); - -SODIUM_EXPORT -int crypto_sign_ed25519ph_final_verify(crypto_sign_ed25519ph_state *state, - const unsigned char *sig, - const unsigned char *pk) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_edwards25519sha512batch.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_edwards25519sha512batch.h deleted file mode 100644 index eed158aa84..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_sign_edwards25519sha512batch.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef crypto_sign_edwards25519sha512batch_H -#define crypto_sign_edwards25519sha512batch_H - -/* - * WARNING: This construction was a prototype, which should not be used - * any more in new projects. - * - * crypto_sign_edwards25519sha512batch is provided for applications - * initially built with NaCl, but as recommended by the author of this - * construction, new applications should use ed25519 instead. - * - * In Sodium, you should use the high-level crypto_sign_*() functions instead. - */ - -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_sign_edwards25519sha512batch_BYTES 64U -#define crypto_sign_edwards25519sha512batch_PUBLICKEYBYTES 32U -#define crypto_sign_edwards25519sha512batch_SECRETKEYBYTES (32U + 32U) -#define crypto_sign_edwards25519sha512batch_MESSAGEBYTES_MAX (SODIUM_SIZE_MAX - crypto_sign_edwards25519sha512batch_BYTES) - -SODIUM_EXPORT -int crypto_sign_edwards25519sha512batch(unsigned char *sm, - unsigned long long *smlen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *sk) - __attribute__ ((deprecated)) __attribute__ ((nonnull(1, 5))); - -SODIUM_EXPORT -int crypto_sign_edwards25519sha512batch_open(unsigned char *m, - unsigned long long *mlen_p, - const unsigned char *sm, - unsigned long long smlen, - const unsigned char *pk) - __attribute__ ((deprecated)) __attribute__ ((nonnull(3, 5))); - -SODIUM_EXPORT -int crypto_sign_edwards25519sha512batch_keypair(unsigned char *pk, - unsigned char *sk) - __attribute__ ((deprecated)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream.h deleted file mode 100644 index 9f3b57f5cc..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream.h +++ /dev/null @@ -1,59 +0,0 @@ -#ifndef crypto_stream_H -#define crypto_stream_H - -/* - * WARNING: This is just a stream cipher. It is NOT authenticated encryption. - * While it provides some protection against eavesdropping, it does NOT - * provide any security against active attacks. - * Unless you know what you're doing, what you are looking for is probably - * the crypto_box functions. - */ - -#include - -#include "crypto_stream_xsalsa20.h" -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_stream_KEYBYTES crypto_stream_xsalsa20_KEYBYTES -SODIUM_EXPORT -size_t crypto_stream_keybytes(void); - -#define crypto_stream_NONCEBYTES crypto_stream_xsalsa20_NONCEBYTES -SODIUM_EXPORT -size_t crypto_stream_noncebytes(void); - -#define crypto_stream_MESSAGEBYTES_MAX crypto_stream_xsalsa20_MESSAGEBYTES_MAX -SODIUM_EXPORT -size_t crypto_stream_messagebytes_max(void); - -#define crypto_stream_PRIMITIVE "xsalsa20" -SODIUM_EXPORT -const char *crypto_stream_primitive(void); - -SODIUM_EXPORT -int crypto_stream(unsigned char *c, unsigned long long clen, - const unsigned char *n, const unsigned char *k) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_stream_xor(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -void crypto_stream_keygen(unsigned char k[crypto_stream_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa20.h deleted file mode 100644 index 27cc007e57..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa20.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef crypto_stream_salsa20_H -#define crypto_stream_salsa20_H - -/* - * WARNING: This is just a stream cipher. It is NOT authenticated encryption. - * While it provides some protection against eavesdropping, it does NOT - * provide any security against active attacks. - * Unless you know what you're doing, what you are looking for is probably - * the crypto_box functions. - */ - -#include -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_stream_salsa20_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_stream_salsa20_keybytes(void); - -#define crypto_stream_salsa20_NONCEBYTES 8U -SODIUM_EXPORT -size_t crypto_stream_salsa20_noncebytes(void); - -#define crypto_stream_salsa20_MESSAGEBYTES_MAX SODIUM_SIZE_MAX -SODIUM_EXPORT -size_t crypto_stream_salsa20_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_stream_salsa20(unsigned char *c, unsigned long long clen, - const unsigned char *n, const unsigned char *k) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_stream_salsa20_xor(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_stream_salsa20_xor_ic(unsigned char *c, const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, uint64_t ic, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 6))); - -SODIUM_EXPORT -void crypto_stream_salsa20_keygen(unsigned char k[crypto_stream_salsa20_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa2012.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa2012.h deleted file mode 100644 index f1ec382555..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa2012.h +++ /dev/null @@ -1,53 +0,0 @@ -#ifndef crypto_stream_salsa2012_H -#define crypto_stream_salsa2012_H - -/* - * WARNING: This is just a stream cipher. It is NOT authenticated encryption. - * While it provides some protection against eavesdropping, it does NOT - * provide any security against active attacks. - * Unless you know what you're doing, what you are looking for is probably - * the crypto_box functions. - */ - -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_stream_salsa2012_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_stream_salsa2012_keybytes(void); - -#define crypto_stream_salsa2012_NONCEBYTES 8U -SODIUM_EXPORT -size_t crypto_stream_salsa2012_noncebytes(void); - -#define crypto_stream_salsa2012_MESSAGEBYTES_MAX SODIUM_SIZE_MAX -SODIUM_EXPORT -size_t crypto_stream_salsa2012_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_stream_salsa2012(unsigned char *c, unsigned long long clen, - const unsigned char *n, const unsigned char *k) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_stream_salsa2012_xor(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -void crypto_stream_salsa2012_keygen(unsigned char k[crypto_stream_salsa2012_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa208.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa208.h deleted file mode 100644 index a7169542e7..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_salsa208.h +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef crypto_stream_salsa208_H -#define crypto_stream_salsa208_H - -/* - * WARNING: This is just a stream cipher. It is NOT authenticated encryption. - * While it provides some protection against eavesdropping, it does NOT - * provide any security against active attacks. - * Unless you know what you're doing, what you are looking for is probably - * the crypto_box functions. - */ - -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_stream_salsa208_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_stream_salsa208_keybytes(void) - __attribute__ ((deprecated)); - -#define crypto_stream_salsa208_NONCEBYTES 8U -SODIUM_EXPORT -size_t crypto_stream_salsa208_noncebytes(void) - __attribute__ ((deprecated)); - -#define crypto_stream_salsa208_MESSAGEBYTES_MAX SODIUM_SIZE_MAX - SODIUM_EXPORT -size_t crypto_stream_salsa208_messagebytes_max(void) - __attribute__ ((deprecated)); - -SODIUM_EXPORT -int crypto_stream_salsa208(unsigned char *c, unsigned long long clen, - const unsigned char *n, const unsigned char *k) - __attribute__ ((deprecated)) __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_stream_salsa208_xor(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((deprecated)) __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -void crypto_stream_salsa208_keygen(unsigned char k[crypto_stream_salsa208_KEYBYTES]) - __attribute__ ((deprecated)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xchacha20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xchacha20.h deleted file mode 100644 index 06f9908a11..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xchacha20.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef crypto_stream_xchacha20_H -#define crypto_stream_xchacha20_H - -/* - * WARNING: This is just a stream cipher. It is NOT authenticated encryption. - * While it provides some protection against eavesdropping, it does NOT - * provide any security against active attacks. - * Unless you know what you're doing, what you are looking for is probably - * the crypto_box functions. - */ - -#include -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_stream_xchacha20_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_stream_xchacha20_keybytes(void); - -#define crypto_stream_xchacha20_NONCEBYTES 24U -SODIUM_EXPORT -size_t crypto_stream_xchacha20_noncebytes(void); - -#define crypto_stream_xchacha20_MESSAGEBYTES_MAX SODIUM_SIZE_MAX -SODIUM_EXPORT -size_t crypto_stream_xchacha20_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_stream_xchacha20(unsigned char *c, unsigned long long clen, - const unsigned char *n, const unsigned char *k) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_stream_xchacha20_xor(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_stream_xchacha20_xor_ic(unsigned char *c, const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, uint64_t ic, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 6))); - -SODIUM_EXPORT -void crypto_stream_xchacha20_keygen(unsigned char k[crypto_stream_xchacha20_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xsalsa20.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xsalsa20.h deleted file mode 100644 index 71d1675df3..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_stream_xsalsa20.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef crypto_stream_xsalsa20_H -#define crypto_stream_xsalsa20_H - -/* - * WARNING: This is just a stream cipher. It is NOT authenticated encryption. - * While it provides some protection against eavesdropping, it does NOT - * provide any security against active attacks. - * Unless you know what you're doing, what you are looking for is probably - * the crypto_box functions. - */ - -#include -#include -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_stream_xsalsa20_KEYBYTES 32U -SODIUM_EXPORT -size_t crypto_stream_xsalsa20_keybytes(void); - -#define crypto_stream_xsalsa20_NONCEBYTES 24U -SODIUM_EXPORT -size_t crypto_stream_xsalsa20_noncebytes(void); - -#define crypto_stream_xsalsa20_MESSAGEBYTES_MAX SODIUM_SIZE_MAX -SODIUM_EXPORT -size_t crypto_stream_xsalsa20_messagebytes_max(void); - -SODIUM_EXPORT -int crypto_stream_xsalsa20(unsigned char *c, unsigned long long clen, - const unsigned char *n, const unsigned char *k) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -int crypto_stream_xsalsa20_xor(unsigned char *c, const unsigned char *m, - unsigned long long mlen, const unsigned char *n, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 5))); - -SODIUM_EXPORT -int crypto_stream_xsalsa20_xor_ic(unsigned char *c, const unsigned char *m, - unsigned long long mlen, - const unsigned char *n, uint64_t ic, - const unsigned char *k) - __attribute__ ((nonnull(1, 4, 6))); - -SODIUM_EXPORT -void crypto_stream_xsalsa20_keygen(unsigned char k[crypto_stream_xsalsa20_KEYBYTES]) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_16.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_16.h deleted file mode 100644 index 7b9c8077ad..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_16.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef crypto_verify_16_H -#define crypto_verify_16_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_verify_16_BYTES 16U -SODIUM_EXPORT -size_t crypto_verify_16_bytes(void); - -SODIUM_EXPORT -int crypto_verify_16(const unsigned char *x, const unsigned char *y) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_32.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_32.h deleted file mode 100644 index 9b0f4529f6..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_32.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef crypto_verify_32_H -#define crypto_verify_32_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_verify_32_BYTES 32U -SODIUM_EXPORT -size_t crypto_verify_32_bytes(void); - -SODIUM_EXPORT -int crypto_verify_32(const unsigned char *x, const unsigned char *y) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_64.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_64.h deleted file mode 100644 index c83b73025a..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_verify_64.h +++ /dev/null @@ -1,23 +0,0 @@ -#ifndef crypto_verify_64_H -#define crypto_verify_64_H - -#include -#include "export.h" - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_verify_64_BYTES 64U -SODIUM_EXPORT -size_t crypto_verify_64_bytes(void); - -SODIUM_EXPORT -int crypto_verify_64(const unsigned char *x, const unsigned char *y) - __attribute__ ((warn_unused_result)) __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake128.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake128.h deleted file mode 100644 index aed16a77f5..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake128.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef crypto_xof_shake128_H -#define crypto_xof_shake128_H - -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -#define crypto_xof_shake128_BLOCKBYTES 168U -SODIUM_EXPORT -size_t crypto_xof_shake128_blockbytes(void); - -#define crypto_xof_shake128_STATEBYTES 256U -SODIUM_EXPORT -size_t crypto_xof_shake128_statebytes(void); - -#define crypto_xof_shake128_DOMAIN_STANDARD 0x1FU -SODIUM_EXPORT -unsigned char crypto_xof_shake128_domain_standard(void); - -typedef struct CRYPTO_ALIGN(16) crypto_xof_shake128_state { - unsigned char opaque[256]; -} crypto_xof_shake128_state; - -SODIUM_EXPORT -int crypto_xof_shake128(unsigned char *out, size_t outlen, const unsigned char *in, - unsigned long long inlen) __attribute__((nonnull(1))); - -SODIUM_EXPORT -int crypto_xof_shake128_init(crypto_xof_shake128_state *state) __attribute__((nonnull)); - -SODIUM_EXPORT -int crypto_xof_shake128_init_with_domain(crypto_xof_shake128_state *state, unsigned char domain) - __attribute__((nonnull)); - -SODIUM_EXPORT -int crypto_xof_shake128_update(crypto_xof_shake128_state *state, - const unsigned char *in, - unsigned long long inlen) __attribute__((nonnull(1))); - -SODIUM_EXPORT -int crypto_xof_shake128_squeeze(crypto_xof_shake128_state *state, unsigned char *out, size_t outlen) - __attribute__((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake256.h deleted file mode 100644 index 9f4bfc7243..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_shake256.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef crypto_xof_shake256_H -#define crypto_xof_shake256_H - -#include "export.h" -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_xof_shake256_BLOCKBYTES 136U -SODIUM_EXPORT -size_t crypto_xof_shake256_blockbytes(void); - -#define crypto_xof_shake256_STATEBYTES 256U -SODIUM_EXPORT -size_t crypto_xof_shake256_statebytes(void); - -#define crypto_xof_shake256_DOMAIN_STANDARD 0x1FU -SODIUM_EXPORT -unsigned char crypto_xof_shake256_domain_standard(void); - -typedef struct CRYPTO_ALIGN(16) crypto_xof_shake256_state { - unsigned char opaque[256]; -} crypto_xof_shake256_state; - -/* One-shot API */ -SODIUM_EXPORT -int crypto_xof_shake256(unsigned char *out, size_t outlen, const unsigned char *in, - unsigned long long inlen) __attribute__((nonnull(1))); - -/* Streaming API with standard domain */ -SODIUM_EXPORT -int crypto_xof_shake256_init(crypto_xof_shake256_state *state) __attribute__((nonnull)); - -/* Streaming API with custom domain */ -SODIUM_EXPORT -int crypto_xof_shake256_init_with_domain(crypto_xof_shake256_state *state, unsigned char domain) - __attribute__((nonnull)); - -SODIUM_EXPORT -int crypto_xof_shake256_update(crypto_xof_shake256_state *state, - const unsigned char *in, - unsigned long long inlen) __attribute__((nonnull(1))); - -SODIUM_EXPORT -int crypto_xof_shake256_squeeze(crypto_xof_shake256_state *state, unsigned char *out, size_t outlen) - __attribute__((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake128.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake128.h deleted file mode 100644 index f8c3e30dd4..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake128.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef crypto_xof_turboshake128_H -#define crypto_xof_turboshake128_H - -#include "export.h" -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_xof_turboshake128_BLOCKBYTES 168U -SODIUM_EXPORT -size_t crypto_xof_turboshake128_blockbytes(void); - -#define crypto_xof_turboshake128_STATEBYTES 256U -SODIUM_EXPORT -size_t crypto_xof_turboshake128_statebytes(void); - -#define crypto_xof_turboshake128_DOMAIN_STANDARD 0x1FU -SODIUM_EXPORT -unsigned char crypto_xof_turboshake128_domain_standard(void); - -typedef struct CRYPTO_ALIGN(16) crypto_xof_turboshake128_state { - unsigned char opaque[256]; -} crypto_xof_turboshake128_state; - -/* One-shot API */ -SODIUM_EXPORT -int crypto_xof_turboshake128(unsigned char *out, size_t outlen, const unsigned char *in, - unsigned long long inlen) __attribute__((nonnull(1))); - -/* Streaming API with standard domain */ -SODIUM_EXPORT -int crypto_xof_turboshake128_init(crypto_xof_turboshake128_state *state) __attribute__((nonnull)); - -/* Streaming API with custom domain */ -SODIUM_EXPORT -int crypto_xof_turboshake128_init_with_domain(crypto_xof_turboshake128_state *state, - unsigned char domain) __attribute__((nonnull)); - -SODIUM_EXPORT -int crypto_xof_turboshake128_update(crypto_xof_turboshake128_state *state, - const unsigned char *in, - unsigned long long inlen) __attribute__((nonnull(1))); - -SODIUM_EXPORT -int crypto_xof_turboshake128_squeeze(crypto_xof_turboshake128_state *state, unsigned char *out, - size_t outlen) __attribute__((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake256.h b/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake256.h deleted file mode 100644 index b325067d10..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/crypto_xof_turboshake256.h +++ /dev/null @@ -1,54 +0,0 @@ -#ifndef crypto_xof_turboshake256_H -#define crypto_xof_turboshake256_H - -#include "export.h" -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define crypto_xof_turboshake256_BLOCKBYTES 136U -SODIUM_EXPORT -size_t crypto_xof_turboshake256_blockbytes(void); - -#define crypto_xof_turboshake256_STATEBYTES 256U -SODIUM_EXPORT -size_t crypto_xof_turboshake256_statebytes(void); - -#define crypto_xof_turboshake256_DOMAIN_STANDARD 0x1FU -SODIUM_EXPORT -unsigned char crypto_xof_turboshake256_domain_standard(void); - -typedef struct CRYPTO_ALIGN(16) crypto_xof_turboshake256_state { - unsigned char opaque[256]; -} crypto_xof_turboshake256_state; - -/* One-shot API */ -SODIUM_EXPORT -int crypto_xof_turboshake256(unsigned char *out, size_t outlen, const unsigned char *in, - unsigned long long inlen) __attribute__((nonnull(1))); - -/* Streaming API with standard domain */ -SODIUM_EXPORT -int crypto_xof_turboshake256_init(crypto_xof_turboshake256_state *state) __attribute__((nonnull)); - -/* Streaming API with custom domain */ -SODIUM_EXPORT -int crypto_xof_turboshake256_init_with_domain(crypto_xof_turboshake256_state *state, - unsigned char domain) __attribute__((nonnull)); - -SODIUM_EXPORT -int crypto_xof_turboshake256_update(crypto_xof_turboshake256_state *state, - const unsigned char *in, - unsigned long long inlen) __attribute__((nonnull(1))); - -SODIUM_EXPORT -int crypto_xof_turboshake256_squeeze(crypto_xof_turboshake256_state *state, unsigned char *out, - size_t outlen) __attribute__((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes.h b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes.h deleted file mode 100644 index c83a4df50b..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes.h +++ /dev/null @@ -1,72 +0,0 @@ - -#ifndef randombytes_H -#define randombytes_H - -#include -#include - -#include - -#include "export.h" - -#ifdef __cplusplus -# ifdef __GNUC__ -# pragma GCC diagnostic ignored "-Wlong-long" -# endif -extern "C" { -#endif - -typedef struct randombytes_implementation { - const char *(*implementation_name)(void); /* required */ - uint32_t (*random)(void); /* required */ - void (*stir)(void); /* optional */ - uint32_t (*uniform)(const uint32_t upper_bound); /* optional, a default implementation will be used if NULL */ - void (*buf)(void * const buf, const size_t size); /* required */ - int (*close)(void); /* optional */ -} randombytes_implementation; - -#define randombytes_BYTES_MAX SODIUM_MIN(SODIUM_SIZE_MAX, 0xffffffffUL) - -#define randombytes_SEEDBYTES 32U -SODIUM_EXPORT -size_t randombytes_seedbytes(void); - -SODIUM_EXPORT -void randombytes_buf(void * const buf, const size_t size) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -void randombytes_buf_deterministic(void * const buf, const size_t size, - const unsigned char seed[randombytes_SEEDBYTES]) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -uint32_t randombytes_random(void); - -SODIUM_EXPORT -uint32_t randombytes_uniform(const uint32_t upper_bound); - -SODIUM_EXPORT -void randombytes_stir(void); - -SODIUM_EXPORT -int randombytes_close(void); - -SODIUM_EXPORT -int randombytes_set_implementation(const randombytes_implementation *impl) - __attribute__ ((nonnull)); - -SODIUM_EXPORT -const char *randombytes_implementation_name(void); - -/* -- NaCl compatibility interface -- */ - -SODIUM_EXPORT -void randombytes(unsigned char * const buf, const unsigned long long buf_len) - __attribute__ ((nonnull)); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_internal_random.h b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_internal_random.h deleted file mode 100644 index 2b2b7d6edc..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_internal_random.h +++ /dev/null @@ -1,22 +0,0 @@ - -#ifndef randombytes_internal_random_H -#define randombytes_internal_random_H - -#include "export.h" -#include "randombytes.h" - -#ifdef __cplusplus -extern "C" { -#endif - -SODIUM_EXPORT -extern struct randombytes_implementation randombytes_internal_implementation; - -/* Backwards compatibility with libsodium < 1.0.18 */ -#define randombytes_salsa20_implementation randombytes_internal_implementation - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_sysrandom.h b/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_sysrandom.h deleted file mode 100644 index 9e27b674c7..0000000000 --- a/quartz/src/nativeInterop/libsodium/include/sodium/randombytes_sysrandom.h +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef randombytes_sysrandom_H -#define randombytes_sysrandom_H - -#include "export.h" -#include "randombytes.h" - -#ifdef __cplusplus -extern "C" { -#endif - -SODIUM_EXPORT -extern struct randombytes_implementation randombytes_sysrandom_implementation; - -#ifdef __cplusplus -} -#endif - -#endif From a463a9802fc265a239ae846c2d25b6dab26c26ce Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 21:36:25 +0100 Subject: [PATCH 027/172] Slightly modify defFile generation, to stop CI cries. --- quartz/build.gradle.kts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 3f08b6634e..fe440ab72b 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -75,9 +75,11 @@ kotlin { val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { val definitionFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") - definitionFile.writeText("package = Clibsodium") - definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") - definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") + if (!definitionFile.exists()){ + definitionFile.writeText("package = Clibsodium") + definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") + definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") + } } listOf( From bfd595ea00d7558a992fcac9736f4ddd8e85b69c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 12 Mar 2026 20:45:10 +0000 Subject: [PATCH 028/172] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hu-rHU/strings.xml | 5 +++++ amethyst/src/main/res/values-pl-rPL/strings.xml | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 92f57ea00c..d1cf8e235a 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -125,6 +125,7 @@ Bájt Hiba Hibák + Az átjátszóhoz való sikeres kapcsolatok százalékos aránya Ebben a munkamenetben lévő kapcsolati hibák száma Fő hírfolyam Privát üzenethírfolyam @@ -645,6 +646,7 @@ Csatlakozás Ma Tartalomra vonatkozó figyelmeztetés + Figyelmeztetés: %1$s Ez a bejegyzés érzékeny tartalmat tartalmaz, amelyet egyesek sértőnek vagy zavarónak találhatnak Az érzékeny tartalmat mindig rejtse el Az érzékeny tartalmat mindig jelenítse meg @@ -832,6 +834,7 @@ Helyszín betöltése… A helyszín-meghatározás nincs engedélyezve Hozzáadja az érzékeny tartalomra vonatkozó figyelmeztetést a tartalom megjelenítése előtt. Ez ideális bármilyen NSFW tartalom vagy olyan tartalom esetén, amelyet egyesek sértőnek vagy zavarónak találhatnak + Indoklás (nem kötelező) Új funkció Ennek az üzemmódnak az aktiválásához az Amethystnek NIP-17 üzenetet kell küldenie (GiftWrapped, Sealed Direct és csoport-üzenetek). A NIP-17 új, és a legtöbb kliens még nem implementálta. Győződjön meg arról is, hogy a kedvezményezett kompatibilis klienst használ. Aktiválás @@ -1503,4 +1506,6 @@ %1$d felhasználói fiók követése… "Írja be egy barátja vagy közösségi vezető profilját. Használhatja az npub, NIP-05 címüket, vagy egy Namecoin nevet, például aliz@pelda.bit vagy id/aliz a blokklánc által ellenőrzött azonosítókhoz." Összes kijelölése + Üzemidő: %1$d%% + Namecoin-beállítások diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 3bf7b6cf66..2efa4044aa 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -1368,6 +1368,7 @@ Kalendarz Spotkanie RSVP Spotkania + Szachy Ulubione Transmitery Określenie kanału Ukryta wiadomość kanału @@ -1460,11 +1461,16 @@ wyszukaj, npub1…, alicja@domena.pl Obsługuje npub, nprofile, NIP-05, hex, i namecoin (.bit, d/, id/) Sprawdź listę obserwowanych + Znaleziono %1$d kont(a) Wybrano: %1$d + Teraz obserwujesz %1$d kont(a) Twój kanał jest gotowy. Pomiń Pobierz więcej Kontynuuj Na razie pomiń + Nie znaleziono obserwowanych + Obserwujesz %1$d kont(a)… + "Wprowadź profil znajomego lub lidera społeczności. Możesz użyć ich npub-u, adresu NIP-05 lub nazwy jak alicja@domena.pl lub id/alicja dla zweryfikowanych przez blockchain tożsamości." Wybierz Wszystkie From 42f48e11fc0f7544a909c9719dc9b8a035dea662 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 21:49:08 +0100 Subject: [PATCH 029/172] Make CI happy(attempt 2). --- quartz/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index fe440ab72b..2a26aaa2db 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -76,6 +76,7 @@ kotlin { val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { val definitionFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") if (!definitionFile.exists()){ + definitionFile.createNewFile() definitionFile.writeText("package = Clibsodium") definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") @@ -90,7 +91,6 @@ kotlin { target.compilations.getByName("main") { // headersTask.get() - libsodiumDefFileGeneration.get() val Clibsodium by cinterops.creating { assert(project.file("Clibsodium.def").exists()) definitionFile.set(project.file("Clibsodium.def")) From 3838b6b815e6fffdd71dfe4b26815676111ab3d4 Mon Sep 17 00:00:00 2001 From: KotlinGeekDev Date: Thu, 12 Mar 2026 22:16:34 +0100 Subject: [PATCH 030/172] Some fixes. --- quartz/build.gradle.kts | 2 - .../nativeInterop/libsodium/include/sodium.h | 54 ------------------- 2 files changed, 56 deletions(-) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 2a26aaa2db..015bc4e39d 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -94,8 +94,6 @@ kotlin { val Clibsodium by cinterops.creating { assert(project.file("Clibsodium.def").exists()) definitionFile.set(project.file("Clibsodium.def")) - includeDirs("${libsodiumPath}/ios-simulators/lib") - //compilerOpts("-I${libsodiumPath}/ios-simulators/lib") headers( "$libsodiumHeaderFilesPath/crypto_aead_xchacha20poly1305.h", diff --git a/quartz/src/nativeInterop/libsodium/include/sodium.h b/quartz/src/nativeInterop/libsodium/include/sodium.h index 61a3168709..60127eb361 100644 --- a/quartz/src/nativeInterop/libsodium/include/sodium.h +++ b/quartz/src/nativeInterop/libsodium/include/sodium.h @@ -5,63 +5,9 @@ #include "sodium/version.h" #include "sodium/core.h" -#include "sodium/crypto_aead_aegis128l.h" -#include "sodium/crypto_aead_aegis256.h" -#include "sodium/crypto_aead_aes256gcm.h" -#include "sodium/crypto_aead_chacha20poly1305.h" #include "sodium/crypto_aead_xchacha20poly1305.h" -#include "sodium/crypto_auth.h" -#include "sodium/crypto_auth_hmacsha256.h" -#include "sodium/crypto_auth_hmacsha512.h" -#include "sodium/crypto_auth_hmacsha512256.h" -#include "sodium/crypto_box.h" -#include "sodium/crypto_box_curve25519xsalsa20poly1305.h" #include "sodium/crypto_core_hchacha20.h" -#include "sodium/crypto_core_hsalsa20.h" -#include "sodium/crypto_core_keccak1600.h" -#include "sodium/crypto_core_salsa20.h" -#include "sodium/crypto_core_salsa2012.h" -#include "sodium/crypto_core_salsa208.h" -#include "sodium/crypto_generichash.h" -#include "sodium/crypto_generichash_blake2b.h" -#include "sodium/crypto_hash.h" -#include "sodium/crypto_hash_sha256.h" -#include "sodium/crypto_hash_sha512.h" -#include "sodium/crypto_ipcrypt.h" -#include "sodium/crypto_kdf.h" -#include "sodium/crypto_kdf_hkdf_sha256.h" -#include "sodium/crypto_kdf_hkdf_sha512.h" -#include "sodium/crypto_kdf_blake2b.h" -#include "sodium/crypto_kdf_hkdf_sha256.h" -#include "sodium/crypto_kdf_hkdf_sha512.h" -#include "sodium/crypto_kx.h" -#include "sodium/crypto_onetimeauth.h" -#include "sodium/crypto_onetimeauth_poly1305.h" -#include "sodium/crypto_pwhash.h" -#include "sodium/crypto_pwhash_argon2i.h" -#include "sodium/crypto_scalarmult.h" -#include "sodium/crypto_scalarmult_curve25519.h" -#include "sodium/crypto_secretbox.h" -#include "sodium/crypto_secretbox_xsalsa20poly1305.h" -#include "sodium/crypto_secretstream_xchacha20poly1305.h" -#include "sodium/crypto_shorthash.h" -#include "sodium/crypto_shorthash_siphash24.h" -#include "sodium/crypto_sign.h" -#include "sodium/crypto_sign_ed25519.h" -#include "sodium/crypto_stream.h" #include "sodium/crypto_stream_chacha20.h" -#include "sodium/crypto_stream_salsa20.h" -#include "sodium/crypto_stream_xsalsa20.h" -#include "sodium/crypto_verify_16.h" -#include "sodium/crypto_verify_32.h" -#include "sodium/crypto_verify_64.h" -#include "sodium/crypto_xof_shake128.h" -#include "sodium/crypto_xof_shake256.h" -#include "sodium/crypto_xof_turboshake128.h" -#include "sodium/crypto_xof_turboshake256.h" -#include "sodium/randombytes.h" -#include "sodium/randombytes_internal_random.h" -#include "sodium/randombytes_sysrandom.h" #include "sodium/runtime.h" #include "sodium/utils.h" From d4dd5a65e6a56ce4239ca33a98c82489ea657526 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 18:56:17 -0400 Subject: [PATCH 031/172] anySync was returning either on true or false. it was not doing what it was supposed to do which is returning after the first true. --- .../quartz/utils/ParallelUtils.kt | 81 +++++++++++++++---- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/ParallelUtils.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/ParallelUtils.kt index 0ffae3d044..1330c303a0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/ParallelUtils.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/ParallelUtils.kt @@ -21,11 +21,15 @@ package com.vitorpamplona.quartz.utils import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.IO import kotlinx.coroutines.async import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeoutOrNull @@ -219,23 +223,24 @@ suspend fun anyAsync( // Use select to wait for the first deferred to complete with 'true' val foundTrue = withTimeoutOrNull(timeoutMillis) { - select { - deferredResults.forEach { deferred -> - // For each deferred, if it completes and its result is 'true', - // this branch of the select expression will be chosen. - deferred.onAwait { result -> - if (result) { - true // Return true from the select expression - } else { - // If a deferred completes with false, we don't want to - // immediately end the select, so we return false, which - // lets select continue waiting for other branches. - false + val remaining = deferredResults.toMutableList() + var found = false + + while (remaining.isNotEmpty() && !found) { + val (winner, value) = + select { + remaining.forEach { deferred -> + deferred.onAwait { value -> + deferred to value + } } } - } + remaining.remove(winner) + if (value) found = true } - } + + found + } ?: false // Once select returns (either with true or after all deferreds complete/are cancelled), // cancel any remaining ongoing operations. @@ -243,5 +248,51 @@ suspend fun anyAsync( // If foundTrue is false, it means all completed with false or were cancelled. deferredResults.forEach { it.cancel() } // Ensure all are cancelled. - return@coroutineScope foundTrue == true + return@coroutineScope foundTrue } + +/** + * Executes a mapping function asynchronously on each input in the list. + * Returns the first result as soon as the first mapping returns not null, cancelling all other ongoing operations. + * + * @param inputs A list of input objects to process. + * @param map A suspend function that takes an input object and returns a Boolean. + * @return True if any mapping function returns true, false otherwise. + */ +suspend fun firstNotNullOrNullAsync( + inputs: List, + timeoutMillis: Long = 30000, + map: suspend (T) -> U?, +): U? { + if (inputs.isEmpty()) { + return null + } + + return withTimeoutOrNull(timeoutMillis) { + val channel = Channel(capacity = Channel.UNLIMITED) + + val jobs = + inputs.map { input -> + launch(Dispatchers.IO) { + val result = map(input) + if (result != null) { + channel.trySend(result) + } + } + } + + // Close channel when all jobs complete (handles all-null case) + launch { + jobs.joinAll() + channel.close() + } + + // Wait for first non-null result or null if channel closes + val result = channel.receiveCatching().getOrNull() + + // Cancel all remaining jobs + jobs.forEach { it.cancel() } + + result + } +} From fa67030b477005629eca97b5ff78072b1389744f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 18:57:46 -0400 Subject: [PATCH 032/172] Adds support for BUD-10 Blossom URIs --- .../com/vitorpamplona/amethyst/AppModules.kt | 76 +++++++-- .../nipB7Blossom/BlossomServerListState.kt | 6 +- .../amethyst/service/images/BlossomFetcher.kt | 91 +++++++++++ .../service/images/ImageLoaderSetup.kt | 3 + .../service/okhttp/DualHttpClientManager.kt | 30 ++-- .../okhttp/DualHttpClientManagerForRelays.kt | 75 +++++++++ .../service/okhttp/IHttpClientManager.kt | 43 +++++ .../service/okhttp/OkHttpClientFactory.kt | 48 +----- .../okhttp/OkHttpClientFactoryForRelays.kt | 116 ++++++++++++++ .../service/playback/diskCache/VideoCache.kt | 11 +- .../playerPool/CustomMediaSourceFactory.kt | 14 +- .../playback/playerPool/ExoPlayerBuilder.kt | 8 +- .../playback/playerPool/MediaSessionPool.kt | 7 +- .../playback/service/PlaybackService.kt | 72 ++++++--- .../relayClient/RelayProxyClientConnector.kt | 4 +- .../blossom/bud10/BlossomServerResolver.kt | 150 ++++++++++++++++++ .../blossom/bud10/OpenBlossomUriAsIntent.kt | 43 +++++ .../uploads/blossom/bud10/ServerHeadCache.kt | 114 +++++++++++++ .../amethyst/ui/components/ClickableUrl.kt | 16 +- .../amethyst/ui/components/ImageGallery.kt | 2 +- .../amethyst/ui/components/LoadUrlPreview.kt | 2 +- .../amethyst/ui/components/MyAsyncImage.kt | 2 +- .../amethyst/ui/components/RichTextViewer.kt | 88 +++++++++- .../ui/components/ZoomableContentView.kt | 46 ++++-- .../ui/note/creators/previews/PreviewUrl.kt | 2 +- .../ui/screen/AccountSessionManager.kt | 2 + .../send/upload/SuccessfulUploads.kt | 2 + amethyst/src/main/res/values/strings.xml | 3 + .../ComposeSubscriptionManager.kt | 41 ++++- .../commons/richtext/RichTextParser.kt | 80 +++++++--- .../richtext/RichTextParserSegments.kt | 9 +- .../amethyst/commons/richtext/UrlParser.kt | 31 ++-- .../commons/richtext/RichTextParserTest.kt | 30 ++-- .../commons/richtext/UrlParserTest.kt | 16 +- .../quartz/nip01Core/core/HexKey.kt | 2 + .../nipB7Blossom/BlossomServersEvent.kt | 5 +- .../quartz/nipB7Blossom/BlossomUri.kt | 17 +- 37 files changed, 1112 insertions(+), 195 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/BlossomFetcher.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManagerForRelays.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/IHttpClientManager.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/BlossomServerResolver.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/OpenBlossomUriAsIntent.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/ServerHeadCache.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 5885c1e221..e538d3fa4f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -25,6 +25,8 @@ import android.content.Context import androidx.security.crypto.EncryptedSharedPreferences import coil3.disk.DiskCache import coil3.memory.MemoryCache +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier @@ -45,6 +47,7 @@ import com.vitorpamplona.amethyst.service.images.ImageLoaderSetup import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.notifications.PokeyReceiver import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager +import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManagerForRelays import com.vitorpamplona.amethyst.service.okhttp.EncryptionKeyCache import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache @@ -54,11 +57,15 @@ import com.vitorpamplona.amethyst.service.relayClient.RelayProxyClientConnector import com.vitorpamplona.amethyst.service.relayClient.authCommand.model.AuthCoordinator import com.vitorpamplona.amethyst.service.relayClient.notifyCommand.model.NotifyCoordinator import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderQueryState +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.UserFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.speedLogger.RelaySpeedLogger +import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver import com.vitorpamplona.amethyst.service.uploads.nip95.Nip95CacheFactory import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.UiSettingsState import com.vitorpamplona.amethyst.ui.tor.TorManager +import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.RelayLogger @@ -73,6 +80,7 @@ import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.DEFAULT_ELECTRUMX_S import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.ElectrumXClient import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.TOR_ELECTRUMX_SERVERS +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -80,6 +88,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch import java.io.File @@ -137,16 +149,6 @@ class AppModules( scope = applicationIOScope, ) - // manages all relay connections - val okHttpClientForRelays = - DualHttpClientManager( - userAgent = appAgent, - proxyPortProvider = torManager.activePortOrNull, - isMobileDataProvider = connManager.isMobileOrNull, - keyCache = keyCache, - scope = applicationIOScope, - ) - // Offers easy methods to know when connections are happening through Tor or not val roleBasedHttpClientBuilder = RoleBasedHttpClientBuilder(okHttpClients, torPrefs.value) @@ -191,6 +193,15 @@ class AppModules( applicationIOScope, ) + // manages all relay connections + val okHttpClientForRelays = + DualHttpClientManagerForRelays( + userAgent = appAgent, + proxyPortProvider = torManager.activePortOrNull, + isMobileDataProvider = connManager.isMobileOrNull, + scope = applicationIOScope, + ) + // Connects the NostrClient class with okHttp val websocketBuilder = OkHttpWebSocket.Builder { url -> @@ -268,6 +279,44 @@ class AppModules( scope = applicationIOScope, ) + fun subscribedFlow( + address: Address, + account: Account, + ): Flow { + val note = cache.getOrCreateAddressableNote(address) + + val userSub = UserFinderQueryState(note.author ?: cache.getOrCreateUser(address.pubKeyHex), account) + val noteSub = EventFinderQueryState(note, account) + + return note + .flow() + .metadata.stateFlow + .onStart { + sources.userFinder.subscribe(userSub) + sources.eventFinder.subscribe(noteSub) + }.onCompletion { + sources.eventFinder.unsubscribe(noteSub) + sources.userFinder.unsubscribe(userSub) + } + } + + val blossomResolver = + BlossomServerResolver( + loggedInUsers = { listOfNotNull(sessionManager.loggedInAccount()?.pubKey) }, + blossomServers = { addressesToSubscribe -> + val account = sessionManager.loggedInAccount() ?: return@BlossomServerResolver listOf() + addressesToSubscribe.map { address -> + subscribedFlow(address, account).transform { + val event = it.note.event as? BlossomServersEvent + if (event != null) { + emit(event) + } + } + } + }, + httpClientBuilder = roleBasedHttpClientBuilder, + ) + // Organizes cache clearing val trimmingService = MemoryTrimmingService(cache) @@ -298,7 +347,12 @@ class AppModules( fun contentResolverFn(): ContentResolver = appContext.contentResolver fun setImageLoader() { - ImageLoaderSetup.setup(appContext, { diskCache }, { memoryCache }) { url -> + ImageLoaderSetup.setup( + app = appContext, + diskCache = { diskCache }, + memoryCache = { memoryCache }, + blossomServerResolver = blossomResolver, + ) { url -> okHttpClients.getHttpClient(roleBasedHttpClientBuilder.shouldUseTorForImageDownload(url)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt index 83aad47327..b2d127f46b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipB7Blossom/BlossomServerListState.kt @@ -70,9 +70,9 @@ class BlossomServerListState( val flow = getBlossomServersListFlow() - .map { normalizeServers(it.note) } - .onStart { emit(normalizeServers(blossomListNote)) } - .flowOn(Dispatchers.IO) + .map { + normalizeServers(it.note) + }.flowOn(Dispatchers.IO) .stateIn( scope, SharingStarted.Eagerly, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/BlossomFetcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/BlossomFetcher.kt new file mode 100644 index 0000000000..327efa264f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/BlossomFetcher.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.images + +import androidx.compose.runtime.Stable +import coil3.ImageLoader +import coil3.Uri +import coil3.annotation.ExperimentalCoilApi +import coil3.fetch.FetchResult +import coil3.fetch.Fetcher +import coil3.network.CacheStrategy +import coil3.network.ConcurrentRequestStrategy +import coil3.network.ConnectivityChecker +import coil3.network.NetworkFetcher +import coil3.network.okhttp.asNetworkClient +import coil3.request.Options +import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver +import okhttp3.Call +import kotlin.coroutines.cancellation.CancellationException + +@Stable +class BlossomFetcher( + private val options: Options, + private val data: Uri, + private val blossomServerResolver: BlossomServerResolver, + private val networkFetcher: (url: String) -> Fetcher, +) : Fetcher { + override suspend fun fetch(): FetchResult? { + println("BlossomFetcher: starting $data") + return try { + val urlResult = blossomServerResolver.findServers(data.toString()) + println("BlossomFetcher: finished $data to ${urlResult?.serverUrl}") + networkFetcher(urlResult?.serverUrl ?: data.toString()).fetch() + } catch (e: Exception) { + if (e is CancellationException) throw e + println("BlossomFetcher: cancelled or error: $e $data") + null + } + } + + @OptIn(ExperimentalCoilApi::class) + class Factory( + val blossomServerResolver: BlossomServerResolver, + val networkClient: (url: String) -> Call.Factory, + ) : Fetcher.Factory { + private val connectivityCheckerLazy = singleParameterLazy(::ConnectivityChecker) + + override fun create( + data: Uri, + options: Options, + imageLoader: ImageLoader, + ): Fetcher? { + println("BlossomFetcher: PreFactory $data") + if (!isApplicable(data)) return null + + println("BlossomFetcher: Factory $data") + + return BlossomFetcher(options, data, blossomServerResolver) { url -> + NetworkFetcher( + url = url, + options = options, + networkClient = lazy { networkClient(url).asNetworkClient() }, + diskCache = lazy { imageLoader.diskCache }, + cacheStrategy = lazy { CacheStrategy.DEFAULT }, + connectivityChecker = lazy { connectivityCheckerLazy.get(options.context) }, + concurrentRequestStrategy = lazy { ConcurrentRequestStrategy.UNCOORDINATED }, + ) + } + } + + private fun isApplicable(data: Uri): Boolean = data.scheme?.lowercase() == "blossom" + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt index dc308f93a8..22a71cd21a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/ImageLoaderSetup.kt @@ -43,6 +43,7 @@ import coil3.svg.SvgDecoder import coil3.util.Logger import coil3.video.VideoFrameDecoder import com.vitorpamplona.amethyst.isDebug +import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver import com.vitorpamplona.quartz.utils.Log import okhttp3.Call @@ -63,6 +64,7 @@ class ImageLoaderSetup { app: Context, diskCache: () -> DiskCache, memoryCache: () -> MemoryCache, + blossomServerResolver: BlossomServerResolver, callFactory: (url: String) -> Call.Factory, ) { SingletonImageLoader.setUnsafe( @@ -78,6 +80,7 @@ class ImageLoaderSetup { add(VideoFrameDecoder.Factory()) add(Base64Fetcher.Factory) add(BlurHashFetcher.Factory) + add(BlossomFetcher.Factory(blossomServerResolver, callFactory)) add(Base64Fetcher.BKeyer) add(BlurHashFetcher.BKeyer) add(OkHttpFactory(callFactory)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt index ab415256f9..b0c4d0ed22 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManager.kt @@ -26,16 +26,12 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import okhttp3.Call import okhttp3.OkHttpClient +import okhttp3.Request import java.net.InetSocketAddress import java.net.Proxy -interface IHttpClientManager { - fun getHttpClient(useProxy: Boolean): OkHttpClient - - fun getCurrentProxyPort(useProxy: Boolean): Int? -} - class DualHttpClientManager( userAgent: String, proxyPortProvider: StateFlow, @@ -79,18 +75,16 @@ class DualHttpClientManager( } else { defaultHttpClientWithoutProxy.value } + + fun getDynamicCallFactory(useProxy: Boolean) = DynamicCallFactory(useProxy, this) } -object EmptyHttpClientManager : IHttpClientManager { - val rootOkHttpClient by lazy { - OkHttpClient - .Builder() - .followRedirects(true) - .followSslRedirects(true) - .build() - } - - override fun getHttpClient(useProxy: Boolean) = rootOkHttpClient - - override fun getCurrentProxyPort(useProxy: Boolean) = null +/** + * the okhttp can change on the manager without affecting other systems. + */ +class DynamicCallFactory( + val useProxy: Boolean, + val manager: DualHttpClientManager, +) : Call.Factory { + override fun newCall(request: Request): Call = manager.getHttpClient(useProxy).newCall(request) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManagerForRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManagerForRelays.kt new file mode 100644 index 0000000000..76413155f7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/DualHttpClientManagerForRelays.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.okhttp + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import okhttp3.OkHttpClient +import java.net.InetSocketAddress +import java.net.Proxy + +class DualHttpClientManagerForRelays( + userAgent: String, + proxyPortProvider: StateFlow, + isMobileDataProvider: StateFlow, + scope: CoroutineScope, +) : IHttpClientManager { + val factory = OkHttpClientFactoryForRelays(userAgent) + + val defaultHttpClient: StateFlow = + combine(proxyPortProvider, isMobileDataProvider) { proxy, mobile -> + factory.buildHttpClient(proxy, mobile) + }.stateIn( + scope, + SharingStarted.WhileSubscribed(1000), + factory.buildHttpClient(proxyPortProvider.value, isMobileDataProvider.value), + ) + + val defaultHttpClientWithoutProxy: StateFlow = + isMobileDataProvider + .map { mobile -> + factory.buildHttpClient(mobile) + }.stateIn( + scope, + SharingStarted.WhileSubscribed(1000), + factory.buildHttpClient(isMobileDataProvider.value), + ) + + fun getCurrentProxy(): Proxy? = defaultHttpClient.value.proxy + + override fun getCurrentProxyPort(useProxy: Boolean): Int? = + if (useProxy) { + (getCurrentProxy()?.address() as? InetSocketAddress)?.port + } else { + null + } + + override fun getHttpClient(useProxy: Boolean): OkHttpClient = + if (useProxy) { + defaultHttpClient.value + } else { + defaultHttpClientWithoutProxy.value + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/IHttpClientManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/IHttpClientManager.kt new file mode 100644 index 0000000000..0c7e07ea90 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/IHttpClientManager.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.okhttp + +import okhttp3.OkHttpClient + +interface IHttpClientManager { + fun getHttpClient(useProxy: Boolean): OkHttpClient + + fun getCurrentProxyPort(useProxy: Boolean): Int? +} + +object EmptyHttpClientManager : IHttpClientManager { + val rootOkHttpClient by lazy { + OkHttpClient + .Builder() + .followRedirects(true) + .followSslRedirects(true) + .build() + } + + override fun getHttpClient(useProxy: Boolean) = rootOkHttpClient + + override fun getCurrentProxyPort(useProxy: Boolean) = null +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 82622f1e39..73a7ff78b5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -20,9 +20,10 @@ */ package com.vitorpamplona.amethyst.service.okhttp -import android.os.Build -import com.vitorpamplona.quartz.utils.Log -import okhttp3.Dispatcher +import com.vitorpamplona.amethyst.service.okhttp.OkHttpClientFactoryForRelays.Companion.DEFAULT_IS_MOBILE +import com.vitorpamplona.amethyst.service.okhttp.OkHttpClientFactoryForRelays.Companion.DEFAULT_SOCKS_PORT +import com.vitorpamplona.amethyst.service.okhttp.OkHttpClientFactoryForRelays.Companion.DEFAULT_TIMEOUT_ON_MOBILE_SECS +import com.vitorpamplona.amethyst.service.okhttp.OkHttpClientFactoryForRelays.Companion.DEFAULT_TIMEOUT_ON_WIFI_SECS import okhttp3.OkHttpClient import java.net.InetSocketAddress import java.net.Proxy @@ -32,53 +33,16 @@ class OkHttpClientFactory( keyCache: EncryptionKeyCache, val userAgent: String, ) { - companion object { - // by picking a random proxy port, the connection will fail as it should. - const val DEFAULT_SOCKS_PORT: Int = 9050 - const val DEFAULT_IS_MOBILE: Boolean = false - const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 - const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 - - private fun isEmulator(): Boolean = - Build.FINGERPRINT.startsWith("generic") || - Build.FINGERPRINT.lowercase().contains("emulator") || - Build.MODEL.contains("google_sdk") || - Build.MODEL.lowercase().contains("droid4x") || - Build.MODEL.contains("Emulator") || - Build.MODEL.contains("Android SDK built for x86") || - Build.MANUFACTURER.contains("Genymotion") || - (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || - "google_sdk" == Build.PRODUCT || - Build.HARDWARE.contains("goldfish") || - Build.HARDWARE.contains("ranchu") || - Build.HARDWARE.contains("vbox86") || - Build.HARDWARE.contains("nox") || - Build.HARDWARE.contains("cuttlefish") - } - - val logging = LoggingInterceptor() + // val logging = LoggingInterceptor() val keyDecryptor = EncryptedBlobInterceptor(keyCache) - val myDispatcher = - Dispatcher().apply { - if (!isEmulator()) { - maxRequestsPerHost = 10 - maxRequests = 1024 - } else { - maxRequestsPerHost = 5 - maxRequests = 256 - Log.i("OkHttpClientFactory", "Emulator detected, using default maxRequests: 64.") - } - } - private val rootClient = OkHttpClient .Builder() - .dispatcher(myDispatcher) .followRedirects(true) .followSslRedirects(true) .addInterceptor(DefaultContentTypeInterceptor(userAgent)) - .addNetworkInterceptor(logging) + // .addNetworkInterceptor(logging) .addNetworkInterceptor(keyDecryptor) .build() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt new file mode 100644 index 0000000000..1729027c32 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactoryForRelays.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.okhttp + +import android.os.Build +import com.vitorpamplona.quartz.utils.Log +import okhttp3.Dispatcher +import okhttp3.OkHttpClient +import java.net.InetSocketAddress +import java.net.Proxy +import java.time.Duration + +class OkHttpClientFactoryForRelays( + userAgent: String, +) { + companion object { + // by picking a random proxy port, the connection will fail as it should. + const val DEFAULT_SOCKS_PORT: Int = 9050 + const val DEFAULT_IS_MOBILE: Boolean = false + const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 + const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 + + private fun isEmulator(): Boolean = + Build.FINGERPRINT.startsWith("generic") || + Build.FINGERPRINT.lowercase().contains("emulator") || + Build.MODEL.contains("google_sdk") || + Build.MODEL.lowercase().contains("droid4x") || + Build.MODEL.contains("Emulator") || + Build.MODEL.contains("Android SDK built for x86") || + Build.MANUFACTURER.contains("Genymotion") || + (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || + "google_sdk" == Build.PRODUCT || + Build.HARDWARE.contains("goldfish") || + Build.HARDWARE.contains("ranchu") || + Build.HARDWARE.contains("vbox86") || + Build.HARDWARE.contains("nox") || + Build.HARDWARE.contains("cuttlefish") + } + + val myDispatcher = + Dispatcher().apply { + if (!isEmulator()) { + maxRequestsPerHost = 10 + maxRequests = 1024 + } else { + maxRequestsPerHost = 5 + maxRequests = 256 + Log.i("OkHttpClientFactory", "Emulator detected, using default maxRequests: 64.") + } + } + + private val rootClient = + OkHttpClient + .Builder() + .dispatcher(myDispatcher) + .followRedirects(true) + .followSslRedirects(true) + .addInterceptor(DefaultContentTypeInterceptor(userAgent)) + .build() + + fun buildHttpClient( + proxy: Proxy?, + timeoutSeconds: Int, + ): OkHttpClient { + val seconds = if (proxy != null) timeoutSeconds * 3 else timeoutSeconds + return rootClient + .newBuilder() + .proxy(proxy) + .connectTimeout(Duration.ofSeconds(seconds.toLong())) + .readTimeout(Duration.ofSeconds(seconds.toLong() * 3)) + .writeTimeout(Duration.ofSeconds(seconds.toLong() * 3)) + .build() + } + + fun buildHttpClient( + localSocksProxyPort: Int?, + isMobile: Boolean?, + ): OkHttpClient = + buildHttpClient( + buildLocalSocksProxy(localSocksProxyPort), + buildTimeout(isMobile ?: DEFAULT_IS_MOBILE), + ) + + fun buildHttpClient(isMobile: Boolean?): OkHttpClient = + buildHttpClient( + null, + buildTimeout(isMobile ?: DEFAULT_IS_MOBILE), + ) + + fun buildTimeout(isMobile: Boolean): Int = + if (isMobile) { + DEFAULT_TIMEOUT_ON_MOBILE_SECS + } else { + DEFAULT_TIMEOUT_ON_WIFI_SECS + } + + fun buildLocalSocksProxy(port: Int?) = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", port ?: DEFAULT_SOCKS_PORT)) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt index d59cac0310..1f2866dd67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/diskCache/VideoCache.kt @@ -23,13 +23,12 @@ package com.vitorpamplona.amethyst.service.playback.diskCache import android.annotation.SuppressLint import android.content.Context import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.DataSource import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor import androidx.media3.datasource.cache.SimpleCache -import androidx.media3.datasource.okhttp.OkHttpDataSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import okhttp3.OkHttpClient import java.io.File @SuppressLint("UnsafeOptInUsageError") @@ -60,18 +59,18 @@ class VideoCache { } // This method should be called when proxy setting changes. - fun renewCacheFactory(client: OkHttpClient) { + fun renewCacheFactory(dataSourceFactory: DataSource.Factory) { cacheDataSourceFactory = CacheDataSource .Factory() .setCache(simpleCache) - .setUpstreamDataSourceFactory(OkHttpDataSource.Factory(client)) + .setUpstreamDataSourceFactory(dataSourceFactory) .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) } - fun get(client: OkHttpClient): CacheDataSource.Factory { + fun get(dataSourceFactory: DataSource.Factory): CacheDataSource.Factory { // Renews the factory because OkHttpMight have changed. - renewCacheFactory(client) + renewCacheFactory(dataSourceFactory) return cacheDataSourceFactory } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt index 51ff47f04e..f04bb625cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/CustomMediaSourceFactory.kt @@ -22,28 +22,26 @@ package com.vitorpamplona.amethyst.service.playback.playerPool import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.okhttp.OkHttpDataSource +import androidx.media3.datasource.DataSource import androidx.media3.exoplayer.drm.DrmSessionManagerProvider import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy -import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache import com.vitorpamplona.amethyst.service.playback.diskCache.isLiveStreaming -import okhttp3.OkHttpClient /** * HLS LiveStreams cannot use cache. */ @UnstableApi class CustomMediaSourceFactory( - okHttpClient: OkHttpClient, + videoCache: VideoCache, + dataSourceFactory: DataSource.Factory, ) : MediaSource.Factory { private var cachingFactory: MediaSource.Factory = - DefaultMediaSourceFactory( - Amethyst.instance.videoCache.get(okHttpClient), - ) + DefaultMediaSourceFactory(videoCache.get(dataSourceFactory)) private var nonCachingFactory: MediaSource.Factory = - DefaultMediaSourceFactory(OkHttpDataSource.Factory(okHttpClient)) + DefaultMediaSourceFactory(dataSourceFactory) override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory { cachingFactory.setDrmSessionManagerProvider(drmSessionManagerProvider) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerBuilder.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerBuilder.kt index 665530fc07..4ceb4497b9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerBuilder.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/ExoPlayerBuilder.kt @@ -23,23 +23,25 @@ package com.vitorpamplona.amethyst.service.playback.playerPool import android.content.Context import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource import androidx.media3.exoplayer.ExoPlayer import com.vitorpamplona.amethyst.model.MediaAspectRatioCache +import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache import com.vitorpamplona.amethyst.service.playback.playerPool.aspectRatio.AspectRatioCacher import com.vitorpamplona.amethyst.service.playback.playerPool.positions.CurrentPlayPositionCacher import com.vitorpamplona.amethyst.service.playback.playerPool.positions.VideoViewedPositionCache import com.vitorpamplona.amethyst.service.playback.playerPool.wake.KeepVideosPlaying -import okhttp3.OkHttpClient @OptIn(UnstableApi::class) class ExoPlayerBuilder( - val okHttp: OkHttpClient, + val videoCache: VideoCache, + val dataSourceFactory: DataSource.Factory, ) { fun build(context: Context): ExoPlayer = ExoPlayer .Builder(context) .apply { - setMediaSourceFactory(CustomMediaSourceFactory(okHttp)) + setMediaSourceFactory(CustomMediaSourceFactory(videoCache, dataSourceFactory)) }.build() .apply { addListener(AspectRatioCacher(MediaAspectRatioCache)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt index 48811b1309..785efc5ec2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/playerPool/MediaSessionPool.kt @@ -29,8 +29,8 @@ import androidx.core.net.toUri import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSourceBitmapLoader -import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import com.google.common.util.concurrent.Futures @@ -41,7 +41,6 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import okhttp3.OkHttpClient class SessionListener( val session: MediaSession, @@ -57,7 +56,7 @@ class SessionListener( */ class MediaSessionPool( val exoPlayerPool: ExoPlayerPool, - val okHttpClient: OkHttpClient, + val dataSourceFactory: DataSource.Factory, val appContext: Context, val reset: (MediaSession, Boolean) -> Unit, ) { @@ -101,7 +100,7 @@ class MediaSessionPool( DataSourceBitmapLoader .Builder(context) .setExecutorService(DataSourceBitmapLoader.DEFAULT_EXECUTOR_SERVICE.get()) - .setDataSourceFactory(OkHttpDataSource.Factory(okHttpClient)) + .setDataSourceFactory(dataSourceFactory) .build(), ) setId(id) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt index b69eaefaec..9b259e69c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/service/PlaybackService.kt @@ -20,35 +20,69 @@ */ package com.vitorpamplona.amethyst.service.playback.service +import android.net.Uri import androidx.annotation.OptIn +import androidx.core.net.toUri import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DataSpec +import androidx.media3.datasource.ResolvingDataSource +import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.service.okhttp.DynamicCallFactory +import com.vitorpamplona.amethyst.service.playback.diskCache.VideoCache import com.vitorpamplona.amethyst.service.playback.pip.BackgroundMedia import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerBuilder import com.vitorpamplona.amethyst.service.playback.playerPool.ExoPlayerPool import com.vitorpamplona.amethyst.service.playback.playerPool.MediaSessionPool import com.vitorpamplona.amethyst.service.playback.playerPool.SimultaneousPlaybackCalculator +import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.BlossomServerResolver import com.vitorpamplona.quartz.utils.Log -import okhttp3.OkHttpClient +import kotlinx.coroutines.runBlocking class PlaybackService : MediaSessionService() { private var poolNoProxy: MediaSessionPool? = null private var poolWithProxy: MediaSessionPool? = null @OptIn(UnstableApi::class) - fun newPool(okHttp: OkHttpClient): MediaSessionPool = - MediaSessionPool( + fun newPool( + videoCache: VideoCache, + okHttpClient: DynamicCallFactory, + blossomServerResolver: BlossomServerResolver, + ): MediaSessionPool { + val dataSourceFactory = OkHttpDataSource.Factory(okHttpClient) + + val resolvingDataSourceFactory: DataSource.Factory = + ResolvingDataSource.Factory( + dataSourceFactory, + ResolvingDataSource.Resolver { dataSpec: DataSpec -> + val originalUri: Uri = dataSpec.uri + val scheme = originalUri.scheme + if (scheme != null && blossomServerResolver.canResolve(scheme)) { + val serverUrl = + runBlocking { + blossomServerResolver.findServers(originalUri.toString()) + } + if (serverUrl != null) { + return@Resolver dataSpec.withUri(serverUrl.serverUrl.toUri()) + } + } + dataSpec + }, + ) + + return MediaSessionPool( exoPlayerPool = ExoPlayerPool( - ExoPlayerBuilder(okHttp), + ExoPlayerBuilder(videoCache, resolvingDataSourceFactory), poolSize = SimultaneousPlaybackCalculator.max(applicationContext), ), - okHttpClient = okHttp, + dataSourceFactory = resolvingDataSourceFactory, appContext = applicationContext, reset = { session, keepPlaying -> (session.player as ExoPlayer).apply { @@ -58,6 +92,7 @@ class PlaybackService : MediaSessionService() { } }, ) + } @OptIn(UnstableApi::class) fun lazyPool(proxyPort: Int): MediaSessionPool { @@ -65,22 +100,23 @@ class PlaybackService : MediaSessionService() { // no proxy poolNoProxy?.let { return it } - // creates new - return newPool(Amethyst.instance.okHttpClients.getHttpClient(false)).also { poolNoProxy = it } - } else { - poolWithProxy?.let { pool -> - // with proxy, check if the port is the same. - val okHttp = Amethyst.instance.okHttpClients.getHttpClient(true) - if (okHttp.proxy != null && okHttp.proxy == pool.exoPlayerPool.builder.okHttp.proxy) { - return pool - } + val okHttpClient = Amethyst.instance.okHttpClients.getDynamicCallFactory(false) + val videoCache = Amethyst.instance.videoCache + val blossomServerResolver = Amethyst.instance.blossomResolver - pool.destroy() - return newPool(okHttp).also { poolWithProxy = it } - } + // creates new + return newPool(videoCache, okHttpClient, blossomServerResolver).also { poolNoProxy = it } + } else { + poolWithProxy?.let { return it } // creates brand new - return newPool(Amethyst.instance.okHttpClients.getHttpClient(true)).also { poolWithProxy = it } + // proxy port can change without affecting the pool because + // the choice of okhttp is resolved in newCall + val okHttpClient = Amethyst.instance.okHttpClients.getDynamicCallFactory(true) + val videoCache = Amethyst.instance.videoCache + val blossomServerResolver = Amethyst.instance.blossomResolver + + return newPool(videoCache, okHttpClient, blossomServerResolver).also { poolWithProxy = it } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt index c72bde6724..512f85a3b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/RelayProxyClientConnector.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient import com.vitorpamplona.amethyst.model.torState.TorRelayEvaluation import com.vitorpamplona.amethyst.service.connectivity.ConnectivityManager import com.vitorpamplona.amethyst.service.connectivity.ConnectivityStatus -import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManager +import com.vitorpamplona.amethyst.service.okhttp.DualHttpClientManagerForRelays import com.vitorpamplona.amethyst.ui.tor.TorManager import com.vitorpamplona.amethyst.ui.tor.TorServiceStatus import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient @@ -45,7 +45,7 @@ import okhttp3.OkHttpClient class RelayProxyClientConnector( val torEvaluator: StateFlow, - val okHttpClients: DualHttpClientManager, + val okHttpClients: DualHttpClientManagerForRelays, val connManager: ConnectivityManager, val torManager: TorManager, val client: INostrClient, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/BlossomServerResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/BlossomServerResolver.kt new file mode 100644 index 0000000000..8bf65ed8e9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/BlossomServerResolver.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.uploads.blossom.bud10 + +import androidx.collection.LruCache +import com.vitorpamplona.amethyst.commons.richtext.mimeTypeMap +import com.vitorpamplona.amethyst.model.privacyOptions.IRoleBasedHttpClientBuilder +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.isValid +import com.vitorpamplona.quartz.nipB7Blossom.BlossomServersEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri +import com.vitorpamplona.quartz.utils.firstNotNullOrNullAsync +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.transformLatest +import kotlinx.coroutines.withTimeoutOrNull +import okhttp3.OkHttpClient +import kotlin.collections.toTypedArray +import kotlin.let + +class BlossomServerResolver( + val loggedInUsers: () -> List, + val blossomServers: (Set

    ) -> List>, + val httpClientBuilder: IRoleBasedHttpClientBuilder, +) { + val blossomHitCache: ServerHeadCache = ServerHeadCache() + val uriToUrlCache = LruCache(200) + + class BlossomUriServer( + val uri: BlossomUri, + val serverUrl: String, + ) + + fun cachedFindServer(uriStr: String): BlossomUriServer? = uriToUrlCache[uriStr] + + suspend fun findServers(uriStr: String): BlossomUriServer? { + uriToUrlCache[uriStr]?.let { return it } + + val result = + withTimeoutOrNull(10000) { + findServersInner(uriStr) + } + + if (result != null) { + uriToUrlCache.put(uriStr, result) + } + + return result + } + + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun findServersInner(uriStr: String): BlossomUriServer? { + val uri = BlossomUri.parse(uriStr) ?: return null + + val expectedMimeType = mimeTypeMap[uri.extension] + val filename = uri.filename() + + if (uri.servers.isNotEmpty()) { + val workingUrl = firstWorkingUrl(uri.servers, filename, expectedMimeType, uri.size) + if (workingUrl != null) { + return BlossomUriServer(uri, workingUrl) + } + } + + val blossomServerConfigNeeded = mutableSetOf
    () + + uri.authors.forEach { + if (it.isValid()) { + blossomServerConfigNeeded.add(BlossomServersEvent.createAddress(it)) + } + } + + loggedInUsers().forEach { + blossomServerConfigNeeded.add(BlossomServersEvent.createAddress(it)) + } + + val flows = + blossomServers(blossomServerConfigNeeded) + .map { blossomServerFlow -> + blossomServerFlow.transformLatest { + val servers = it.servers() + if (servers.isNotEmpty()) { + firstWorkingUrl(servers, filename, expectedMimeType, uri.size)?.let { serverUrl -> + emit(serverUrl) + } + } + } + }.toTypedArray() + + if (flows.isNotEmpty()) { + val serverResult = merge(*flows).first() + return BlossomUriServer(uri, serverResult) + } + + return null + } + + private suspend fun firstWorkingUrl( + servers: List, + filename: String, + expectedMimeType: String?, + expectedSize: Long?, + ): String? = + firstNotNullOrNullAsync(servers, 10000) { + blossomHitCache.urlIfServerHasFile(it, filename, expectedMimeType, expectedSize) { url -> + client(url, expectedMimeType) + } + } + + fun client( + url: String, + mimeType: String?, + ): OkHttpClient = + if (mimeType == null) { + httpClientBuilder.okHttpClientForPreview(url) + } else if (mimeType.startsWith("audio/") || mimeType.startsWith("video/")) { + httpClientBuilder.okHttpClientForVideo(url) + } else if (mimeType.startsWith("image/")) { + httpClientBuilder.okHttpClientForImage(url) + } else { + httpClientBuilder.okHttpClientForPreview(url) + } + + fun canResolve(scheme: String) = scheme == SCHEME + + companion object { + const val SCHEME = "blossom" + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/OpenBlossomUriAsIntent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/OpenBlossomUriAsIntent.kt new file mode 100644 index 0000000000..d494d8ea6c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/OpenBlossomUriAsIntent.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.uploads.blossom.bud10 + +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri +import com.vitorpamplona.amethyst.R +import kotlin.coroutines.cancellation.CancellationException + +fun openBlossomUriAsIntent( + context: Context, + blossomUri: String, + onError: (Int, Int) -> Unit, +) { + try { + val intent = Intent(Intent.ACTION_VIEW, blossomUri.toUri()) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + + context.startActivity(intent) + } catch (e: Exception) { + if (e is CancellationException) throw e + onError(R.string.no_blossom_apps_found_title, R.string.no_blossom_apps_found_description) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/ServerHeadCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/ServerHeadCache.kt new file mode 100644 index 0000000000..4653c50afc --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/uploads/blossom/bud10/ServerHeadCache.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.uploads.blossom.bud10 + +import androidx.collection.LruCache +import kotlinx.coroutines.CancellationException +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.coroutines.executeAsync + +class ServerHeadCache { + val cache = LruCache(200) + + sealed interface HasFile { + object NoFile : HasFile + + class TypeAndSize( + val mimeType: String, + val size: Long, + ) : HasFile + } + + suspend fun getFileSizeBytes( + url: String, + client: (url: String) -> OkHttpClient, + ): HasFile { + cache[url]?.let { return it } + + try { + // Build a HEAD request instead of GET + val request = + Request + .Builder() + .url(url) + .head() // Specifies the HEAD method + .build() + + client(url).newCall(request).executeAsync().use { response -> + if (!response.isSuccessful) { + cache.put(url, HasFile.NoFile) + return HasFile.NoFile + } + + // Retrieve the "Content-Length" header + val contentLength = response.header("Content-Length")?.toLongOrNull() + val mimeType = response.header("Content-Type")?.toMediaType()?.toString() + + if (contentLength != null && mimeType != null) { + val result = HasFile.TypeAndSize(mimeType, contentLength) + cache.put(url, result) + return result + } else { + cache.put(url, HasFile.NoFile) + return HasFile.NoFile + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + cache.put(url, HasFile.NoFile) + return HasFile.NoFile + } + } + + suspend fun urlIfServerHasFile( + server: String, + filename: String, + expectedMimeType: String?, + expectedSize: Long?, + client: (url: String) -> OkHttpClient, + ): String? { + val url = + if (server.startsWith("http")) { + server.removeSuffix("/") + "/" + filename + } else { + "https://" + server.removeSuffix("/") + "/" + filename + } + + val result = getFileSizeBytes(url, client) + + if (result is HasFile.TypeAndSize) { + if (expectedSize == null && expectedMimeType == null) { + // any match goes + return url + } else { + if (result.size == expectedSize) { + return url + } + if (expectedSize == null && result.size > 0 && result.mimeType == expectedMimeType) { + return url + } + } + } + return null + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt index b0fe1b9741..86e111ac27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableUrl.kt @@ -21,24 +21,32 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent @Composable fun ClickableUrl( urlText: String, url: String, + onError: (Int, Int) -> Unit = { _, _ -> }, ) { val uri = LocalUriHandler.current + val context = LocalContext.current ClickableTextPrimary( text = urlText, maxLines = 1, - overflow = TextOverflow.Ellipsis, + overflow = TextOverflow.MiddleEllipsis, onClick = { - runCatching { - val doubleCheckedUrl = if (url.contains("://")) url else "https://$url" - uri.openUri(doubleCheckedUrl) + if (url.startsWith("blossom:")) { + openBlossomUriAsIntent(context, url, onError) + } else { + runCatching { + val doubleCheckedUrl = if (url.contains("://")) url else "https://$url" + uri.openUri(doubleCheckedUrl) + } } }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt index 1a815ac609..450125d2a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ImageGallery.kt @@ -144,7 +144,7 @@ fun ImageGallery( images.words .mapNotNull { segment -> val imageUrl = segment.segmentText - state.imagesForPager[imageUrl] as? MediaUrlImage + state.mediaForPager[imageUrl] as? MediaUrlImage }.toImmutableList() Column(modifier = modifier.padding(vertical = Size10dp)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt index d620de76a9..b99fb85e96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/LoadUrlPreview.kt @@ -76,7 +76,7 @@ fun LoadUrlPreviewDirect( is UrlPreviewState.Loading -> { WaitAndDisplay { - DisplayUrlWithLoadingSymbol(url) + DisplayUrlWithLoadingSymbol(url, accountViewModel.toastManager::toast) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt index 9c29244c4a..13d94ae4c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/MyAsyncImage.kt @@ -82,7 +82,7 @@ fun MyAsyncImage( LoadingAnimation(Size40dp, Size6dp) } } else { - DisplayUrlWithLoadingSymbol(imageUrl) + DisplayUrlWithLoadingSymbol(imageUrl, accountViewModel.toastManager::toast) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 1dd39aa232..fe147eb19d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalLayoutDirection @@ -54,18 +55,21 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextMeasurer import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.compose.produceCachedState import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.amethyst.commons.model.EmptyTagList import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.commons.richtext.Base64Segment import com.vitorpamplona.amethyst.commons.richtext.BechSegment +import com.vitorpamplona.amethyst.commons.richtext.BlossomUriSegment import com.vitorpamplona.amethyst.commons.richtext.CashuSegment import com.vitorpamplona.amethyst.commons.richtext.EmailSegment import com.vitorpamplona.amethyst.commons.richtext.EmojiSegment @@ -93,6 +97,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon import com.vitorpamplona.amethyst.service.CachedRichTextParser import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo +import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.markdown.RenderContentAsMarkdown import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav @@ -112,6 +117,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.inlinePlaceholder import com.vitorpamplona.amethyst.ui.theme.innerPostModifier import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -170,6 +176,10 @@ fun RenderStrangeNamePreview() { ClickableRelayUrl(word.segmentText, EmptyNav()) } + is BlossomUriSegment -> { + ClickableRelayUrl(word.segmentText, EmptyNav()) + } + is SchemelessUrlSegment -> { NoProtocolUrlRenderer(word.segmentText) } @@ -500,6 +510,8 @@ private fun RenderWordWithoutPreview( is RelayUrlSegment -> ClickableRelayUrl(word.segmentText, nav) + is BlossomUriSegment -> BlossomUriRendererNoPreview(word.segmentText, accountViewModel) + is SchemelessUrlSegment -> NoProtocolUrlRenderer(word.segmentText) } } @@ -532,19 +544,91 @@ private fun RenderWordWithPreview( is RegularTextSegment -> Text(word.segmentText) is Base64Segment -> ZoomableContentView(word.segmentText, state, accountViewModel) is RelayUrlSegment -> ClickableRelayUrl(word.segmentText, nav) + is BlossomUriSegment -> BlossomUriRenderer(word.segmentText, state, callbackUri, accountViewModel) is SchemelessUrlSegment -> NoProtocolUrlRenderer(word.segmentText) } } +@Composable +fun BlossomUriRenderer( + word: String, + state: RichTextViewerState, + callbackUri: String? = null, + accountViewModel: AccountViewModel, +) { + val isMedia = state.mediaForPager.contains(word) + + if (isMedia) { + ZoomableContentView(word, state, accountViewModel) + } else { + val serverResultState = + remember(word) { + mutableStateOf(Amethyst.instance.blossomResolver.cachedFindServer(word)) + } + + if (serverResultState.value == null) { + LaunchedEffect(word) { + serverResultState.value = Amethyst.instance.blossomResolver.findServers(word) + } + } + + val serverResult = serverResultState.value + if (serverResult != null && serverResult.serverUrl.isNotBlank()) { + LoadUrlPreview(serverResult.serverUrl, serverResult.uri.filename(), callbackUri, accountViewModel) + } else { + ClickableBlossomUri(word, accountViewModel) + } + } +} + +@Composable +fun ClickableBlossomUri( + blossomUri: String, + accountViewModel: AccountViewModel, +) { + val context = LocalContext.current + + ClickableTextPrimary( + text = remember { BlossomUri.parse(blossomUri)?.filename() ?: blossomUri }, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + onClick = { openBlossomUriAsIntent(context, blossomUri, accountViewModel.toastManager::toast) }, + ) +} + +@Composable +fun BlossomUriRendererNoPreview( + word: String, + accountViewModel: AccountViewModel, +) { + val serverResultState = + remember(word) { + mutableStateOf(Amethyst.instance.blossomResolver.cachedFindServer(word)) + } + + if (serverResultState.value == null) { + LaunchedEffect(word) { + serverResultState.value = Amethyst.instance.blossomResolver.findServers(word) + } + } + + val serverResult = serverResultState.value + if (serverResult != null && serverResult.serverUrl.isNotBlank()) { + ClickableUrl(serverResult.uri.filename(), serverResult.serverUrl) + } else { + ClickableBlossomUri(word, accountViewModel) + } +} + @Composable private fun ZoomableContentView( word: String, state: RichTextViewerState, accountViewModel: AccountViewModel, ) { - state.imagesForPager[word]?.let { + state.mediaForPager[word]?.let { Box(modifier = HalfVertPadding) { - ZoomableContentView(it, state.imageList, roundedCorner = true, contentScale = ContentScale.FillWidth, accountViewModel) + ZoomableContentView(it, state.mediaList, roundedCorner = true, contentScale = ContentScale.FillWidth, accountViewModel) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt index e998711aec..e9f22e2abc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZoomableContentView.kt @@ -86,6 +86,7 @@ import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo import com.vitorpamplona.amethyst.model.MediaAspectRatioCache import com.vitorpamplona.amethyst.service.images.BlurhashWrapper import com.vitorpamplona.amethyst.service.playback.composable.VideoView +import com.vitorpamplona.amethyst.service.uploads.blossom.bud10.openBlossomUriAsIntent import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.InformationDialog import com.vitorpamplona.amethyst.ui.note.BlankNote @@ -286,7 +287,7 @@ fun LocalImageView( } } else { WaitAndDisplay { - DisplayUrlWithLoadingSymbol(content) + DisplayUrlWithLoadingSymbol(content, accountViewModel.toastManager::toast) } } } @@ -408,7 +409,7 @@ fun UrlImageView( } } else { WaitAndDisplay { - DisplayUrlWithLoadingSymbol(content) + DisplayUrlWithLoadingSymbol(content, accountViewModel.toastManager::toast) } } } @@ -580,7 +581,10 @@ fun WaitAndDisplay(content: @Composable (AnimatedVisibilityScope.() -> Unit)) { } @Composable -fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) { +fun DisplayUrlWithLoadingSymbol( + content: BaseMediaContent, + onError: (Int, Int) -> Unit = { _, _ -> }, +) { val uri = LocalUriHandler.current val primary = MaterialTheme.colorScheme.primary @@ -589,6 +593,8 @@ fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) { val regularText = remember { SpanStyle(color = background) } val clickableTextStyle = remember { SpanStyle(color = primary) } + val context = LocalContext.current + val annotatedTermsString = remember { buildAnnotatedString { @@ -614,7 +620,13 @@ fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) { val pressIndicator = remember { if (content is MediaUrlContent) { - Modifier.clickable { runCatching { uri.openUri(content.url) } } + Modifier.clickable { + if (content.url.startsWith("blossom:")) { + openBlossomUriAsIntent(context, content.url, onError) + } else { + runCatching { uri.openUri(content.url) } + } + } } else { Modifier } @@ -628,10 +640,8 @@ fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) { ) { Text( text = annotatedTermsString, - modifier = - pressIndicator - .weight(1f, fill = false), - overflow = TextOverflow.Ellipsis, + modifier = pressIndicator.weight(1f, fill = false), + overflow = TextOverflow.MiddleEllipsis, maxLines = 1, ) InlineLoadingIcon() @@ -639,7 +649,10 @@ fun DisplayUrlWithLoadingSymbol(content: BaseMediaContent) { } @Composable -fun DisplayUrlWithLoadingSymbol(url: String) { +fun DisplayUrlWithLoadingSymbol( + url: String, + onError: (Int, Int) -> Unit = { _, _ -> }, +) { val uri = LocalUriHandler.current val primary = MaterialTheme.colorScheme.primary @@ -654,7 +667,20 @@ fun DisplayUrlWithLoadingSymbol(url: String) { } } - val pressIndicator = remember { Modifier.clickable { runCatching { uri.openUri(url) } } } + val context = LocalContext.current + + val pressIndicator = + remember { + Modifier.clickable { + if (url.startsWith("blossom:")) { + openBlossomUriAsIntent(context, url, onError) + } else { + runCatching { + uri.openUri(url) + } + } + } + } Row( modifier = Modifier.width(IntrinsicSize.Max), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt index f3a1c3956c..8f4b6ac897 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/previews/PreviewUrl.kt @@ -292,7 +292,7 @@ private fun MyLoadUrlPreviewDirectFillWidth( is UrlPreviewState.Loading -> { WaitAndDisplay { - DisplayUrlWithLoadingSymbol(url) + DisplayUrlWithLoadingSymbol(url, accountViewModel.toastManager::toast) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt index 3c8dd191e1..3549f612ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountSessionManager.kt @@ -101,6 +101,8 @@ class AccountSessionManager( private val _accountContent = MutableStateFlow(AccountState.Loading) val accountContent = _accountContent.asStateFlow() + fun loggedInAccount() = (_accountContent.value as? AccountState.LoggedIn)?.account + fun loginWithDefaultAccountIfLoggedOff() { // pulls account from storage. if (_accountContent.value !is AccountState.LoggedIn) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt index cc510cc249..c88223ab0f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/SuccessfulUploads.kt @@ -20,9 +20,11 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload +import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.quartz.utils.ciphers.AESGCM +@Stable class SuccessfulUploads( val result: UploadOrchestrator.OrchestratorResult.ServerResult, val caption: String?, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 799135f7dc..149a5c53d2 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1081,6 +1081,9 @@ No Wallets found to pay a lightning invoice (Error: %1$s). Please install a Lightning wallet to use zaps No Wallets found to pay a lightning invoice. Please install a Lightning wallet to use zaps + Can\'t open Blossom links + Blossom apps were not found. Please install a local Blossom app to see this file + Hidden Words Hide new word or sentence Profile Picture diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt index 9a5465811a..b7787ff5c5 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/relayClient/composeSubscriptionManagers/ComposeSubscriptionManager.kt @@ -21,17 +21,20 @@ package com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers import java.util.concurrent.ConcurrentHashMap +import kotlin.collections.forEach /** * This allows composables to directly register their queries * to relays. There may be multiple duplications in these * subscriptions since we do not control when screens are removed. */ -abstract class ComposeSubscriptionManager : ComposeSubscriptionManagerControls { +abstract class ComposeSubscriptionManager : + ComposeSubscriptionManagerControls, + Subscribable { private var composeSubscriptions: ConcurrentHashMap = ConcurrentHashMap() // This is called by main. Keep it really fast. - fun subscribe(query: T?) { + override fun subscribe(query: T?) { if (query == null) return composeSubscriptions.put(query, query) @@ -40,7 +43,7 @@ abstract class ComposeSubscriptionManager : ComposeSubscriptionManagerControl } // This is called by main. Keep it really fast. - fun unsubscribe(query: T?) { + override fun unsubscribe(query: T?) { if (query == null) return composeSubscriptions.remove(query) @@ -48,5 +51,37 @@ abstract class ComposeSubscriptionManager : ComposeSubscriptionManagerControl invalidateKeys() } + override fun subscribe(query: List) { + if (query.isEmpty()) return + + query.forEach { + composeSubscriptions.put(it, it) + } + + invalidateKeys() + } + + // This is called by main. Keep it really fast. + override fun unsubscribe(query: List) { + if (query.isEmpty()) return + + query.forEach { + composeSubscriptions.remove(it) + } + + invalidateKeys() + } + fun allKeys() = composeSubscriptions.keys } + +interface Subscribable { + // This is called by main. Keep it really fast. + fun subscribe(query: T?) + + fun unsubscribe(query: T?) + + fun subscribe(query: List) + + fun unsubscribe(query: List) +} diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 7f39960b69..c1dd4975a4 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.richtext import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists +import com.vitorpamplona.amethyst.commons.richtext.mimeTypeMap import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip31Alts.AltTag @@ -161,27 +162,35 @@ class RichTextParser { val emojiMap = CustomEmoji.createEmojiMap(tags.lists) - val allUrls = urlSet.withScheme + urlSet.withoutScheme + urlSet.emails + urlSet.bech32s + urlSet.relayUrls + val allUrls = urlSet.withScheme + urlSet.withoutScheme + urlSet.emails + urlSet.bech32s + urlSet.relayUrls + urlSet.blossomUris val newContent = fixMissingSpaces(content, allUrls) val segments = findTextSegments(newContent, imageUrls, videoUrls, urlSet, emojiMap, tags) - val base64Images = segments.flatMap { it.words.filterIsInstance() } - val mediaForPagerWithBase64 = mediaForPager + - base64Images - .mapNotNull { createMediaContent(it.segmentText, emptyMap(), content, callbackUri) } - .associateBy { it.url } + segments + .flatMap { paragraph -> + paragraph.words + .mapNotNull { + if (it is Base64Segment) { + createMediaContent(it.segmentText, emptyMap(), content, callbackUri) + } else if (it is BlossomUriSegment) { + createMediaContent(it.segmentText, emptyMap(), content, callbackUri) + } else { + null + } + } + }.associateBy { it.url } return RichTextViewerState( - urlSet, - mediaForPagerWithBase64.toImmutableMap(), - mediaForPagerWithBase64.values.toImmutableList(), - emojiMap.toImmutableMap(), - segments, - tags, + urlSet = urlSet, + mediaForPager = mediaForPagerWithBase64.toImmutableMap(), + mediaList = mediaForPagerWithBase64.values.toImmutableList(), + customEmoji = emojiMap.toImmutableMap(), + paragraphs = segments, + tags = tags, ) } @@ -290,6 +299,8 @@ class RichTextParser { if (urls.relayUrls.contains(word)) return RelayUrlSegment(word) + if (urls.blossomUris.contains(word)) return BlossomUriSegment(word) + if (startsWithNIP19Scheme(word)) return BechSegment(word) if (CustomEmoji.fastMightContainEmoji(word, emojis) && emojis.any { word.contains(it.key) }) return EmojiSegment(word) @@ -359,25 +370,12 @@ class RichTextParser { companion object { val longDatePattern: Regex = Regex("^\\d{4}-\\d{2}-\\d{2}$") val shortDatePattern: Regex = Regex("^\\d{2}-\\d{2}-\\d{2}$") - val numberPattern: Regex = Regex("^(-?[\\d.]+)([a-zA-Z%]*)$") val noProtocolUrlValidator = Regex( "(([a-zA-Z0-9_-]+@)?([a-zA-Z0-9_-]+\\.)*[a-zA-Z0-9_-]+[\\.\\:][a-zA-Z0-9_]+([\\/ \\?\\=\\&\\#\\.]?[a-zA-Z0-9_-]+)*\\/?)(.*)", ) - // Splits at spaces AND at ASCII/multibyte character boundaries - // e.g. "ああexample.com" -> ["ああ", "example.com"] - val wordBoundaryRegex = Regex("(?<=[\\u0000-\\u00FF])(?=[\\u0100-\\uFFFF])|(?<=[\\u0100-\\uFFFF])(?=[\\u0000-\\u00FF])| +") - - val additionalUrlSchema = - """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""" - .toRegex(RegexOption.IGNORE_CASE) - - val HTTPRegex = - "^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?" - .toRegex(RegexOption.IGNORE_CASE) - val imageExt = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg", "avif") val videoExt = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3", "m3u8", "ogg", "wav", "flac", "aac", "opus", "m4a") @@ -403,6 +401,10 @@ class RichTextParser { fullUrl } + fun isImageExtension(ext: String) = imageExtensions.any { it == ext } + + fun isImageOrVideoExtension(ext: String) = imageExtensions.any { it == ext } || videoExtensions.any { it == ext } + fun isImageOrVideoUrl(url: String): Boolean { val removedParamsFromUrl = removeQueryParamsForExtensionComparison(url) @@ -462,3 +464,31 @@ class RichTextParser { fun isUrlWithoutScheme(url: String) = noProtocolUrlValidator.matches(url) } } + +val mimeTypeMap: Map = + mapOf( + // Images + "png" to "image/png", + "jpg" to "image/jpeg", + "jpeg" to "image/jpeg", + "gif" to "image/gif", + "bmp" to "image/bmp", + "webp" to "image/webp", + "svg" to "image/svg+xml", + "avif" to "image/avif", + "tiff" to "image/tiff", + // Video + "mp4" to "video/mp4", + "webm" to "video/webm", + "ogg" to "video/ogg", + "mov" to "video/quicktime", + "avi" to "video/x-msvideo", + "mkv" to "video/x-matroska", + // Audio + "mp3" to "audio/mpeg", + "wav" to "audio/wav", + "ogg" to "audio/ogg", + "m4a" to "audio/mp4", + "aac" to "audio/aac", + "flac" to "audio/flac", + ) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt index b56a47b19f..79de431fdd 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt @@ -28,8 +28,8 @@ import kotlinx.collections.immutable.ImmutableMap @Immutable class RichTextViewerState( val urlSet: Urls, - val imagesForPager: ImmutableMap, - val imageList: ImmutableList, + val mediaForPager: ImmutableMap, + val mediaList: ImmutableList, val customEmoji: ImmutableMap, val paragraphs: ImmutableList, val tags: ImmutableListOfLists, @@ -143,6 +143,11 @@ class RelayUrlSegment( segment: String, ) : Segment(segment) +@Immutable +class BlossomUriSegment( + segment: String, +) : Segment(segment) + @Immutable class SchemelessUrlSegment( segment: String, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParser.kt index df78d34574..9c6c6007ba 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParser.kt @@ -33,8 +33,10 @@ class Urls( val emails: Set = emptySet(), val bech32s: Set = emptySet(), val relayUrls: Set = emptySet(), + val blossomUris: Set = emptySet(), ) +val httpScheme = listOf(DualCase("http")) val websocketScheme = listOf(DualCase("ws")) val nostrScheme = listOf(DualCase("nostr")) val blossomScheme = listOf(DualCase("blossom")) @@ -72,25 +74,31 @@ class UrlParser { val emails = mutableSetOf() val bech32 = mutableSetOf() val relays = mutableSetOf() + val blossom = mutableSetOf() - urls.forEach { - if (it.isValidTopLevelDomain()) { - if (it.wroteWithSchema()) { - if (it.originalUrl.startsWithAny(nostrScheme)) { - bech32.add(it.originalUrl) - } else if (it.originalUrl.startsWithAny(websocketScheme)) { - relays.add(it.originalUrl) + urls.forEach { url -> + if (url.isValidTopLevelDomain()) { + if (url.wroteWithSchema()) { + if (url.originalUrl.startsWithAny(httpScheme)) { + // quick exit + completeUrls.add(url.originalUrl) + } else if (url.originalUrl.startsWithAny(nostrScheme)) { + bech32.add(url.originalUrl) + } else if (url.originalUrl.startsWithAny(websocketScheme)) { + relays.add(url.originalUrl) + } else if (url.originalUrl.startsWithAny(blossomScheme)) { + blossom.add(url.originalUrl) } else { - completeUrls.add(it.originalUrl) + completeUrls.add(url.originalUrl) } } else { // emails are understood as urls from the detector. - if (it.isEmail()) { - Patterns.EMAIL_ADDRESS.findAll(it.originalUrl).forEach { + if (url.isEmail()) { + Patterns.EMAIL_ADDRESS.findAll(url.originalUrl).forEach { emails.add(it.value) } } else { - urlsWithoutScheme.add(it.originalUrl) + urlsWithoutScheme.add(url.originalUrl) } } } @@ -102,6 +110,7 @@ class UrlParser { emails = emails, bech32s = bech32, relayUrls = relays, + blossomUris = blossom, ) } } diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt index 0fc584c52d..745576d5f4 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserTest.kt @@ -4050,8 +4050,8 @@ class RichTextParserTest { ) } - assertTrue(state.imagesForPager.isEmpty()) - assertTrue(state.imageList.isEmpty()) + assertTrue(state.mediaForPager.isEmpty()) + assertTrue(state.mediaList.isEmpty()) assertTrue(state.customEmoji.isEmpty()) assertEquals(651, state.paragraphs.size) } @@ -4064,8 +4064,8 @@ class RichTextParserTest { assertTrue(state.urlSet.withoutScheme.isEmpty()) assertTrue(state.urlSet.withScheme.isEmpty()) assertTrue(state.urlSet.emails.isEmpty()) - assertTrue(state.imagesForPager.isEmpty()) - assertTrue(state.imageList.isEmpty()) + assertTrue(state.mediaForPager.isEmpty()) + assertTrue(state.mediaList.isEmpty()) assertTrue(state.customEmoji.isEmpty()) assertEquals( "Hi, how are you doing?", @@ -4084,8 +4084,8 @@ class RichTextParserTest { assertTrue(state.urlSet.withoutScheme.isEmpty()) assertTrue(state.urlSet.withScheme.isEmpty()) assertTrue(state.urlSet.emails.isEmpty()) - assertTrue(state.imagesForPager.isEmpty()) - assertTrue(state.imageList.isEmpty()) + assertTrue(state.mediaForPager.isEmpty()) + assertTrue(state.mediaList.isEmpty()) assertTrue(state.customEmoji.isEmpty()) assertEquals( "\nHi,\nhow\n\n\n are you doing?\n", @@ -4107,17 +4107,29 @@ class RichTextParserTest { val state = RichTextParser() .parseText(text, EmptyTagList, null) + + val urls = state.urlSet.withScheme.toList() + val bech = state.urlSet.bech32s.toList() + assertEquals( "https://lnshort.it/live-stream-embeds/", - state.urlSet.withScheme.firstOrNull(), + urls[0], ) assertEquals( "https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", - state.imagesForPager.keys.firstOrNull(), + urls[1], + ) + assertEquals( + "nostr:npub1048qg5p6kfnpth2l98kq3dffg097tutm4npsz2exygx25ge2k9xqf5x3nf", + bech[0], ) assertEquals( "https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", - state.imageList.firstOrNull()?.url, + state.mediaForPager.keys.firstOrNull(), + ) + assertEquals( + "https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db214e8a.jpg", + state.mediaList.firstOrNull()?.url, ) assertTrue(state.customEmoji.isEmpty()) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt index ce27e3ef4c..ffe539242f 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.richtext +import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri import kotlin.test.Test import kotlin.test.assertEquals @@ -286,9 +287,14 @@ class UrlParserTest { ) @Test - fun testBlossom() = - test( - "blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.satellite.earth", - Urls(withScheme = setOf("blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.satellite.earth")), - ) + fun testBlossom() { + val blossom = "blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.satellite.earth" + test(blossom, Urls(blossomUris = mapOf(blossom to BlossomUri.parse(blossom)!!))) + } + + @Test + fun testBlossomComplete() { + val blossom = "blossom:a7b3c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1.png?xs=cdn.example.com&xs=media.nostr.build&as=781208004e09102d7da3b7345e64fd193cd1bc3fce8fdae6008d77f9cabcd036&as=b53185b9f27962ebdf76b8a9b0a84cd8b27f9f3d4abd59f715788a3bf9e7f75e&sz=2547831" + test(blossom, Urls(blossomUris = mapOf(blossom to BlossomUri.parse(blossom)!!))) + } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt index d3c4c29b30..e7fefe331b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/HexKey.kt @@ -31,6 +31,8 @@ fun HexKey.hexToByteArray(): ByteArray = Hex.decode(this) fun HexKey.hexToByteArrayOrNull(): ByteArray? = if (Hex.isHex(this)) Hex.decode(this) else null +fun HexKey.isValid(): Boolean = length == PUBKEY_LENGTH && Hex.isHex(this) + const val PUBKEY_LENGTH = 64 const val EVENT_ID_LENGTH = 64 diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt index 394f5576be..2b34a31a8a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomServersEvent.kt @@ -59,9 +59,8 @@ class BlossomServersEvent( fun createTagArray(servers: List): Array> = servers - .map { - arrayOf("server", it) - }.plusElement(AltTag.assemble(ALT)) + .map { arrayOf("server", it) } + .plusElement(AltTag.assemble(ALT)) .toTypedArray() suspend fun updateRelayList( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUri.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUri.kt index b2c06663c8..bbd34156d2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUri.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nipB7Blossom/BlossomUri.kt @@ -20,7 +20,9 @@ */ package com.vitorpamplona.quartz.nipB7Blossom +import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.utils.Hex /** * Parsed representation of a BUD-10 Blossom URI. @@ -33,6 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey * @param authors Hex pubkeys of blob uploaders used for BUD-03 server-list lookup (`as` params). * @param size Blob size in bytes for verification and progress display (`sz` param). */ +@Stable data class BlossomUri( val sha256: HexKey, val extension: String, @@ -40,6 +43,17 @@ data class BlossomUri( val authors: List, val size: Long?, ) { + fun filename(): String = "$sha256.$extension" + + fun toServerUrl(): String? { + val server = servers.firstOrNull()?.removeSuffix("/") ?: return null + return if (server.startsWith("http")) { + "$server/$sha256.$extension" + } else { + "https://$server/$sha256.$extension" + } + } + /** * Serialises back to a canonical `blossom:` URI string. * Server URLs are percent-encoded so that `&`, `=`, and `#` inside them @@ -65,7 +79,6 @@ data class BlossomUri( companion object { private const val SCHEME = "blossom:" - private val SHA256_REGEX = Regex("^[0-9a-f]{64}$") /** * Parses a BUD-10 URI string into a [BlossomUri], or returns `null` if the @@ -93,7 +106,7 @@ data class BlossomUri( extension = "bin" } - if (!SHA256_REGEX.matches(sha256)) return null + if (sha256.length != 64 || !Hex.isHex64(sha256)) return null // Collect repeated query parameters. val servers = mutableListOf() From ebbd7e1a7785b9bf0be9031149c8677d1497598d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 19:06:17 -0400 Subject: [PATCH 033/172] adds today's work to the change log --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a6fc15052..dd274bcac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,12 +21,14 @@ Adds support for NIP-52 Calendar appointments Adds support for NIP-39 External Identities with kind 10011 +Adds support for NIP-C0 Code Snippets + Adds support for NIP-66 Relay Monitor and discovery support to Quartz -Adds support for NIP-C0 Code Snippets to Quartz - Adds support for NIP-A3 Payment targets (PayTo: 10133) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 +Adds support for BUD-10 "Blossom:" URIs + Adds support for Namecoin .bit urls to NIP-05 - Adds choice of ElectrumX server to resolve namecoins. @@ -42,6 +44,10 @@ Removes support for NIP-96 and updates Blossom recommendations Adds support to upload Documents to all new post screens. +Content warning improvements: +- Adds optional description field for sensitive content warnings in new posts. +- Displays additional information on warning composables. + Redesigns and reorganizes Setting pages - Consolidate drawer settings into a single Settings hub screen - Redesigns Zap Amount and NWC setup screens @@ -109,6 +115,8 @@ Fixes: - Fixes crash when getting OpenGraph tags of invalid URLs - Fixes NIP-44 key mutation in NIP-46 connect - Location permission watcher moved outside screens to avoid recreation +- Solves the sorting contract crash on search by precaching all values before sorting users. +- Fixes lingering relay connections from loading follows outbox's settings. AI: - Add SKILL.md for AI agent customization @@ -136,6 +144,7 @@ Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7 - Adds NIP-46 Bunker Login - Adds Support for Chess - Adds Thread Screens +- Adds advanced search with query engine and filter panel - Adds encrypted DMs (NIP-04/NIP-17) - Adds proper empty states with EOSE tracking - Adds multi-column deck layout From 21e29bbb3cb5cfa5aa5eaa102bebe9e99721f35d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 19:42:46 -0400 Subject: [PATCH 034/172] Fixes test cases. --- .../vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt index ffe539242f..9fb2ef84ac 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.richtext -import com.vitorpamplona.quartz.nipB7Blossom.BlossomUri import kotlin.test.Test import kotlin.test.assertEquals @@ -289,12 +288,12 @@ class UrlParserTest { @Test fun testBlossom() { val blossom = "blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.satellite.earth" - test(blossom, Urls(blossomUris = mapOf(blossom to BlossomUri.parse(blossom)!!))) + test(blossom, Urls(blossomUris = setOf(blossom))) } @Test fun testBlossomComplete() { val blossom = "blossom:a7b3c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1.png?xs=cdn.example.com&xs=media.nostr.build&as=781208004e09102d7da3b7345e64fd193cd1bc3fce8fdae6008d77f9cabcd036&as=b53185b9f27962ebdf76b8a9b0a84cd8b27f9f3d4abd59f715788a3bf9e7f75e&sz=2547831" - test(blossom, Urls(blossomUris = mapOf(blossom to BlossomUri.parse(blossom)!!))) + test(blossom, Urls(blossomUris = setOf(blossom))) } } From 0d9f390cd90f243e5b47446990216f7d6aa8cc30 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 19:44:51 -0400 Subject: [PATCH 035/172] missing tests fix --- .../vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt index 9fb2ef84ac..b9613c5cd7 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/UrlParserTest.kt @@ -34,6 +34,9 @@ class UrlParserTest { assertEquals(expected.withScheme, urlSet.withScheme) assertEquals(expected.withoutScheme, urlSet.withoutScheme) assertEquals(expected.emails, urlSet.emails) + assertEquals(expected.bech32s, urlSet.bech32s) + assertEquals(expected.blossomUris, urlSet.blossomUris) + assertEquals(expected.relayUrls, urlSet.relayUrls) } @Test From d9eb44df1f33632d20d6184580ef5d2db7d15107 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 19:52:36 -0400 Subject: [PATCH 036/172] Improving the spacing in the new translation settings screen --- .../ui/screen/loggedIn/settings/UserSettingsScreen.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index 3237243e68..2f9c5717ff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -63,6 +63,8 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.SpacedBy10dp +import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import java.util.Locale as JavaLocale @@ -93,6 +95,7 @@ fun UserSettingsScreen( .padding(top = Size10dp, start = Size20dp, end = Size20dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = SpacedBy10dp, ) { DontTranslateFromSetting(accountViewModel) TranslateToSetting(accountViewModel) @@ -219,7 +222,7 @@ fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) { if (languagePreferences.isEmpty()) return - Column { + Column(verticalArrangement = SpacedBy5dp) { SettingsRow( name = R.string.language_preferences, description = R.string.language_preferences_description, From 198aa0971ca858ae6d55fedf98f4ef24cc24542c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 12 Mar 2026 22:13:33 -0400 Subject: [PATCH 037/172] adds ./gradlew spotlessApply request to claude improves description of the .md file --- .claude/CLAUDE.md | 26 ++++++++++++++++++++------ .claude/settings.json | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 72b5c7264e..8c87760cd3 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,8 +1,14 @@ -# Amethyst Desktop Fork +# Amethyst ## Project Overview -Fork of [Amethyst](https://github.com/vitorpamplona/amethyst) adding Compose Multiplatform Desktop support. Quartz library converted to full KMP for code sharing between Android and Desktop JVM. +Amethyst is a Nostr Client for Android that was made for Android-only and has been slowly switching +over to a Kotlin Multiplatform project. This project has 4 main modules: `quartz`, `commons`, +`amethyst` and `desktopApp`. Quartz should contain implementations of Nostr specifications and +utilities to help implement them. Commons stores shared code between Amethyst Android (`amethyst`) +and Amethyst Desktop (`desktopApp`). The Desktop App is designed to be mouse first and so uses a +completely different screen and navigation architecture while sharing the back end components with +the android counterpart. ## Architecture @@ -12,7 +18,8 @@ amethyst/ │ └── src/ │ ├── commonMain/ # Shared Nostr protocol, data models │ ├── androidMain/ # Android-specific (crypto, storage) -│ └── jvmMain/ # Desktop JVM-specific +│ ├── jvmMain/ # Desktop JVM-specific +│ └── iosMain/ # iOS-specific ├── commons/ # Shared UI components (convert to KMP) │ └── src/ │ ├── commonMain/ # Shared composables, icons, state @@ -20,12 +27,12 @@ amethyst/ │ └── jvmMain/ # Desktop-specific UI utilities ├── desktopApp/ # Desktop JVM application (layouts, navigation) ├── amethyst/ # Android app (layouts, navigation) -└── ammolite/ # Support module +└── ammolite/ # Support module (unused) ``` **Sharing Philosophy:** -- `quartz/` = Business logic, protocol, data (no UI) -- `commons/` = Shared UI components, icons, composables, **ViewModels** +- `quartz/` = Nostr business logic, protocol, data (no UI) +- `commons/` = Shared UI components, icons, composables, flows and ViewModels - `amethyst/` & `desktopApp/` = Platform-native layouts and navigation ## Tech Stack @@ -241,6 +248,13 @@ actual fun openExternalUrl(url: String) { } ``` +## Code Formatting +After completing any task that modifies Kotlin files, always run: +``` +./gradlew spotlessApply +``` +Do this before considering the task complete. + ### Navigation Shell - **Desktop**: Sidebar + main content area - **Android**: Bottom navigation diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..fb09a8e87d --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "Stop": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "./gradlew spotlessApply", + "timeout": 120 + } + ] + } + ] + } +} \ No newline at end of file From 7746561360479f328b06ad7cc12024facc239015 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 02:29:35 +0000 Subject: [PATCH 038/172] feat: refresh translation settings UI with modern components - Translate To: shows all available languages with searchable list instead of only device locales - Don't Translate From: uses InputChip FlowRow with an Add button and searchable language picker to add new languages - Language Preferences: displays as OutlinedCards with radio buttons for choosing display language, includes delete and add-new-pair functionality with guided source/target selection - Added new string resources for the refreshed UI https://claude.ai/code/session_011iTJ1gg8weaxaALiUQNH4C --- .../loggedIn/settings/UserSettingsScreen.kt | 606 +++++++++++++----- amethyst/src/main/res/values/strings.xml | 8 + 2 files changed, 446 insertions(+), 168 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index 2f9c5717ff..34ec325474 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -20,28 +20,43 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings -import android.content.res.Resources +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.SwapHoriz import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.IconButton +import androidx.compose.material3.InputChip +import androidx.compose.material3.InputChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -49,10 +64,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.os.ConfigurationCompat import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -64,7 +78,6 @@ import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.SpacedBy10dp -import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import java.util.Locale as JavaLocale @@ -97,141 +110,219 @@ fun UserSettingsScreen( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = SpacedBy10dp, ) { - DontTranslateFromSetting(accountViewModel) TranslateToSetting(accountViewModel) + HorizontalDivider(thickness = DividerThickness) + DontTranslateFromSetting(accountViewModel) + HorizontalDivider(thickness = DividerThickness) LanguagePreferencesSetting(accountViewModel) } } } } -@OptIn(ExperimentalMaterial3Api::class) +private fun getAllLanguagesSorted(): List { + val seen = mutableSetOf() + return JavaLocale.getAvailableLocales() + .filter { it.language.isNotBlank() && it.country.isBlank() && seen.add(it.language) } + .sortedBy { it.displayName.lowercase() } +} + @Composable -fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { - var expanded by remember { mutableStateOf(false) } - val selectedLanguages = accountViewModel.dontTranslateFromFilteredBySpokenLanguages().toMutableSet() +private fun SearchableLanguageList( + languages: List, + onSelect: (JavaLocale) -> Unit, + modifier: Modifier = Modifier, +) { + var searchQuery by remember { mutableStateOf("") } + val filtered = + remember(searchQuery, languages) { + if (searchQuery.isBlank()) { + languages + } else { + languages.filter { + it.displayName.contains(searchQuery, ignoreCase = true) || + it.language.contains(searchQuery, ignoreCase = true) + } + } + } - Column { - SettingsRow( - name = R.string.dont_translate_from, - description = R.string.dont_translate_from_description, - ) { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, - ) { - OutlinedTextField( - value = stringRes(R.string.quick_action_select), - onValueChange = {}, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable), + Column(modifier) { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { Text(stringRes(R.string.search_languages)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null, + modifier = Modifier.size(20.dp), ) + }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, + Spacer(modifier = Modifier.height(4.dp)) + + LazyColumn(modifier = Modifier.heightIn(max = 200.dp)) { + items(filtered, key = { it.language }) { locale -> + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { onSelect(locale) } + .padding(horizontal = 8.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, ) { - selectedLanguages.forEach { languageCode -> - DropdownMenuItem( - text = { Text(text = JavaLocale.forLanguageTag(languageCode).displayName) }, - onClick = { - accountViewModel.toggleDontTranslateFrom(languageCode) - selectedLanguages.remove(languageCode) - expanded = false - }, - trailingIcon = { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringRes(R.string.remove_language, languageCode), - tint = Color.Red, - modifier = Modifier.size(16.dp), - ) - }, - ) - } + Text( + text = locale.displayName, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.weight(1f), + ) + Text( + text = locale.language, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun TranslateToSetting(accountViewModel: AccountViewModel) { - var expanded by remember { mutableStateOf(false) } val currentTranslateTo = accountViewModel.translateTo() - val languageList = ConfigurationCompat.getLocales(Resources.getSystem().configuration) + val allLanguages = remember { getAllLanguagesSorted() } + var showPicker by remember { mutableStateOf(false) } - Column { + Column(modifier = Modifier.fillMaxWidth()) { SettingsRow( name = R.string.translate_to, description = R.string.translate_to_description, ) { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, + OutlinedCard( + modifier = Modifier.clickable { showPicker = !showPicker }, ) { - OutlinedTextField( - value = JavaLocale(currentTranslateTo).displayName, - onValueChange = {}, - readOnly = true, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable), - ) - - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - for (i in 0 until languageList.size()) { - languageList.get(i)?.let { lang -> - DropdownMenuItem( - text = { - Row(verticalAlignment = Alignment.CenterVertically) { - if (accountViewModel.account.settings.translateToContains(lang)) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - } else { - Spacer(modifier = Modifier.size(24.dp)) - } - Spacer(modifier = Modifier.size(10.dp)) - Text(text = lang.displayName) - } - }, - onClick = { - accountViewModel.updateTranslateTo(lang) - expanded = false - }, - ) - } - } + Text( + text = JavaLocale(currentTranslateTo).displayName, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + ) } } } + + if (showPicker) { + Spacer(modifier = Modifier.height(8.dp)) + + SearchableLanguageList( + languages = allLanguages, + onSelect = { locale -> + accountViewModel.updateTranslateTo(locale) + showPicker = false + }, + ) + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { + val selectedLanguages = accountViewModel.dontTranslateFrom().toSet() + var showAddPicker by remember { mutableStateOf(false) } + val allLanguages = remember { getAllLanguagesSorted() } + + val availableToAdd = + remember(selectedLanguages, allLanguages) { + allLanguages.filter { it.language !in selectedLanguages } + } + + Column(modifier = Modifier.fillMaxWidth()) { + SettingsRow( + name = R.string.dont_translate_from, + description = R.string.dont_translate_from_description, + ) {} + + Spacer(modifier = Modifier.height(8.dp)) + + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth(), + ) { + selectedLanguages.forEach { languageCode -> + InputChip( + selected = true, + onClick = { accountViewModel.toggleDontTranslateFrom(languageCode) }, + label = { Text(JavaLocale(languageCode).displayName) }, + trailingIcon = { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringRes(R.string.remove_language, languageCode), + modifier = Modifier.size(InputChipDefaults.IconSize), + ) + }, + ) + } + + InputChip( + selected = false, + onClick = { showAddPicker = !showAddPicker }, + label = { Text(stringRes(R.string.add_language)) }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(InputChipDefaults.IconSize), + ) + }, + ) + } + + if (showAddPicker) { + Spacer(modifier = Modifier.height(8.dp)) + + SearchableLanguageList( + languages = availableToAdd, + onSelect = { locale -> + accountViewModel.toggleDontTranslateFrom(locale.language) + showAddPicker = false + }, + ) + } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) { val languagePreferences = accountViewModel.account.settings.syncedSettings.languages.languagePreferences + var showAddPair by remember { mutableStateOf(false) } - if (languagePreferences.isEmpty()) return - - Column(verticalArrangement = SpacedBy5dp) { + Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { SettingsRow( name = R.string.language_preferences, description = R.string.language_preferences_description, ) {} + if (languagePreferences.isEmpty() && !showAddPair) { + Text( + text = stringRes(R.string.no_language_preferences), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 8.dp), + ) + } + languagePreferences.forEach { (key, preference) -> val parts = key.split(",") if (parts.size == 2) { - LanguagePreferenceItem( + LanguagePreferenceCard( source = parts[0], target = parts[1], preference = preference, @@ -239,12 +330,31 @@ fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) { ) } } + + TextButton( + onClick = { showAddPair = !showAddPair }, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(4.dp)) + Text(stringRes(R.string.add_language_pair)) + } + + if (showAddPair) { + AddLanguagePairCard( + accountViewModel = accountViewModel, + onDismiss = { showAddPair = false }, + ) + } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun LanguagePreferenceItem( +private fun LanguagePreferenceCard( source: String, target: String, preference: String, @@ -252,77 +362,237 @@ private fun LanguagePreferenceItem( ) { val sourceName = JavaLocale(source).displayName val targetName = JavaLocale(target).displayName - var expanded by remember { mutableStateOf(false) } - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, - modifier = Modifier.padding(vertical = 4.dp), - ) { - OutlinedTextField( - value = stringRes(R.string.language_preference_pair, sourceName, targetName), - onValueChange = {}, - readOnly = true, - label = { - Text( - if (preference == source) { - stringRes(R.string.translations_show_in_lang_first, sourceName) - } else { - stringRes(R.string.translations_show_in_lang_first, targetName) - }, + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Default.SwapHoriz, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), ) - }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable), - ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringRes(R.string.language_preference_pair, sourceName, targetName), + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.weight(1f), + ) + IconButton( + onClick = { + accountViewModel.prefer(source, target, preference) + }, + modifier = Modifier.size(32.dp), + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringRes(R.string.delete_preference), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(18.dp), + ) + } + } - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - DropdownMenuItem( - text = { - Row(verticalAlignment = Alignment.CenterVertically) { - if (preference == source) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - } else { - Spacer(modifier = Modifier.size(24.dp)) - } - Spacer(modifier = Modifier.size(10.dp)) - Text(stringRes(R.string.translations_show_in_lang_first, sourceName)) - } - }, - onClick = { - accountViewModel.prefer(source, target, source) - expanded = false - }, - ) - HorizontalDivider(thickness = DividerThickness) - DropdownMenuItem( - text = { - Row(verticalAlignment = Alignment.CenterVertically) { - if (preference == target) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(24.dp), - ) - } else { - Spacer(modifier = Modifier.size(24.dp)) - } - Spacer(modifier = Modifier.size(10.dp)) - Text(stringRes(R.string.translations_show_in_lang_first, targetName)) - } - }, - onClick = { - accountViewModel.prefer(source, target, target) - expanded = false - }, - ) + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { accountViewModel.prefer(source, target, source) } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = preference == source, + onClick = { accountViewModel.prefer(source, target, source) }, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringRes(R.string.show_first, sourceName), + style = MaterialTheme.typography.bodyMedium, + ) + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { accountViewModel.prefer(source, target, target) } + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = preference == target, + onClick = { accountViewModel.prefer(source, target, target) }, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringRes(R.string.show_first, targetName), + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } +} + +@Composable +private fun AddLanguagePairCard( + accountViewModel: AccountViewModel, + onDismiss: () -> Unit, +) { + val allLanguages = remember { getAllLanguagesSorted() } + var selectedSource by remember { mutableStateOf(null) } + var selectedTarget by remember { mutableStateOf(null) } + var pickingSource by remember { mutableStateOf(true) } + var pickingTarget by remember { mutableStateOf(false) } + + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(12.dp)) { + Text( + text = stringRes(R.string.add_language_pair), + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Medium, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + OutlinedCard( + modifier = + Modifier + .weight(1f) + .clickable { + pickingSource = true + pickingTarget = false + }, + ) { + Column(modifier = Modifier.padding(8.dp)) { + Text( + text = stringRes(R.string.source_language), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = selectedSource?.displayName ?: stringRes(R.string.quick_action_select), + style = MaterialTheme.typography.bodyMedium, + fontWeight = if (selectedSource != null) FontWeight.Medium else FontWeight.Normal, + color = + if (selectedSource != null) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + } + + Icon( + imageVector = Icons.Default.SwapHoriz, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + + OutlinedCard( + modifier = + Modifier + .weight(1f) + .clickable { + pickingTarget = true + pickingSource = false + }, + ) { + Column(modifier = Modifier.padding(8.dp)) { + Text( + text = stringRes(R.string.target_language), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = selectedTarget?.displayName ?: stringRes(R.string.quick_action_select), + style = MaterialTheme.typography.bodyMedium, + fontWeight = if (selectedTarget != null) FontWeight.Medium else FontWeight.Normal, + color = + if (selectedTarget != null) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + } + } + + if (pickingSource) { + Spacer(modifier = Modifier.height(8.dp)) + SearchableLanguageList( + languages = allLanguages, + onSelect = { locale -> + selectedSource = locale + pickingSource = false + if (selectedTarget == null) { + pickingTarget = true + } + }, + ) + } + + if (pickingTarget) { + Spacer(modifier = Modifier.height(8.dp)) + SearchableLanguageList( + languages = allLanguages.filter { it.language != selectedSource?.language }, + onSelect = { locale -> + selectedTarget = locale + pickingTarget = false + }, + ) + } + + if (selectedSource != null && selectedTarget != null) { + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton(onClick = onDismiss) { + Text(stringRes(R.string.go_back)) + } + + Spacer(modifier = Modifier.width(8.dp)) + + TextButton( + onClick = { + val src = selectedSource!!.language + val tgt = selectedTarget!!.language + accountViewModel.prefer(src, tgt, tgt) + onDismiss() + }, + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + stringRes( + R.string.show_first, + selectedTarget!!.displayName, + ), + ) + } + } + } } } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 72f3724dc7..262549ec40 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1508,6 +1508,14 @@ Language Display Preferences For each translated language pair, choose which language to show first. %1$s → %2$s + Search languages + Add language + Add language pair + Source language + Target language + Show %1$s first + No language display preferences yet. These are created automatically when translations occur, or you can add them manually. + Delete preference Pause Play Open dropdown menu From 997a270a84ee7269c4d0dfb58968aaa3d83ba749 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 02:30:33 +0000 Subject: [PATCH 039/172] feat: add export button to relay settings screen Adds a share icon button in the top bar of the relay settings screen that exports all relay configurations (except Connected Relays) as human-readable text via Android's share intent. Each section includes a header and description, with one relay URL per line. https://claude.ai/code/session_013PhuahpBFh6djVHzSsmNKM --- .../ui/navigation/topbars/ActionTopBar.kt | 6 + .../loggedIn/relays/AllRelayListScreen.kt | 193 ++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 2 + 3 files changed, 201 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt index a16b5925e3..a49514dc17 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/ActionTopBar.kt @@ -43,6 +43,7 @@ fun ActionTopBar( isActive: () -> Boolean = { true }, onCancel: () -> Unit, onPost: () -> Unit, + additionalActions: @Composable (() -> Unit)? = null, ) { ShorterTopAppBar( title = { @@ -63,6 +64,9 @@ fun ActionTopBar( ) }, actions = { + if (additionalActions != null) { + additionalActions() + } Button( modifier = HalfHorzPadding, enabled = isActive(), @@ -100,12 +104,14 @@ fun SavingTopBar( isActive: () -> Boolean = { true }, onCancel: () -> Unit, onPost: () -> Unit, + additionalActions: @Composable (() -> Unit)? = null, ) = ActionTopBar( titleRes = titleRes, postRes = R.string.save, isActive = isActive, onCancel = onCancel, onPost = onPost, + additionalActions = additionalActions, ) @OptIn(ExperimentalMaterial3Api::class) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 508c8ae092..ef41607dee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays +import android.content.Context +import android.content.Intent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets @@ -27,7 +29,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Scaffold @@ -36,6 +42,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -77,6 +84,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.theme.grayText @Composable @@ -175,10 +183,36 @@ fun MappedAllRelayListView( val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle() + val context = LocalContext.current + Scaffold( topBar = { SavingTopBar( titleRes = R.string.relay_settings, + additionalActions = { + IconButton(onClick = { + exportRelaySettings( + context = context, + homeRelays = homeFeedState, + notifRelays = notifFeedState, + dmRelays = dmFeedState, + privateOutboxRelays = privateOutboxFeedState, + proxyRelays = proxyRelays, + broadcastRelays = broadcastRelays, + indexerRelays = indexerRelays, + searchRelays = searchFeedState, + localRelays = localFeedState, + trustedRelays = trustedFeedState, + favoriteRelays = relayFeedsFeedState, + blockedRelays = blockedFeedState, + ) + }) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringRes(R.string.export_relay_settings), + ) + } + }, onCancel = { dmViewModel.clear() nip65ViewModel.clear() @@ -447,3 +481,162 @@ fun SettingsCategoryWithButton( action() } } + +private fun formatRelaySection( + title: String, + description: String, + relays: List, + builder: StringBuilder, +) { + if (relays.isEmpty()) return + builder.appendLine("## $title") + builder.appendLine("# $description") + builder.appendLine() + relays.forEach { relay -> + builder.appendLine(relay.relay.url) + } + builder.appendLine() +} + +fun buildRelaySettingsText( + context: Context, + homeRelays: List, + notifRelays: List, + dmRelays: List, + privateOutboxRelays: List, + proxyRelays: List, + broadcastRelays: List, + indexerRelays: List, + searchRelays: List, + localRelays: List, + trustedRelays: List, + favoriteRelays: List, + blockedRelays: List, +): String { + val builder = StringBuilder() + builder.appendLine("# ${context.getString(R.string.relay_settings)}") + builder.appendLine() + + formatRelaySection( + context.getString(R.string.public_home_section), + context.getString(R.string.public_home_section_explainer), + homeRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.public_notif_section), + context.getString(R.string.public_notif_section_explainer), + notifRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.private_inbox_section), + context.getString(R.string.private_inbox_section_explainer), + dmRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.private_outbox_section), + context.getString(R.string.private_outbox_section_explainer), + privateOutboxRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.proxy_section), + context.getString(R.string.proxy_section_explainer), + proxyRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.broadcast_section), + context.getString(R.string.broadcast_section_explainer), + broadcastRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.indexer_section), + context.getString(R.string.indexer_section_explainer), + indexerRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.search_section), + context.getString(R.string.search_section_explainer), + searchRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.local_section), + context.getString(R.string.local_section_explainer), + localRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.trusted_section), + context.getString(R.string.trusted_section_explainer), + trustedRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.favorite_section), + context.getString(R.string.favorite_section_explainer), + favoriteRelays, + builder, + ) + formatRelaySection( + context.getString(R.string.blocked_section), + context.getString(R.string.blocked_section_explainer), + blockedRelays, + builder, + ) + + return builder.toString().trimEnd() +} + +fun exportRelaySettings( + context: Context, + homeRelays: List, + notifRelays: List, + dmRelays: List, + privateOutboxRelays: List, + proxyRelays: List, + broadcastRelays: List, + indexerRelays: List, + searchRelays: List, + localRelays: List, + trustedRelays: List, + favoriteRelays: List, + blockedRelays: List, +) { + val text = + buildRelaySettingsText( + context, + homeRelays, + notifRelays, + dmRelays, + privateOutboxRelays, + proxyRelays, + broadcastRelays, + indexerRelays, + searchRelays, + localRelays, + trustedRelays, + favoriteRelays, + blockedRelays, + ) + + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + putExtra(Intent.EXTRA_TITLE, context.getString(R.string.export_relay_settings)) + } + + val shareIntent = + Intent.createChooser( + sendIntent, + context.getString(R.string.export_relay_settings), + ) + context.startActivity(shareIntent) +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 72f3724dc7..02349fa0df 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1379,6 +1379,8 @@ Blocked Relays Amethyst will never connect to these relays + Export relay settings + Zap the Devs! Your donation helps us make a difference. Every sat counts! Donate Now From 48485ce933ef18538aaf040d73a41d7e8d5c6bc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 02:49:07 +0000 Subject: [PATCH 040/172] feat: add confirmation dialog when swiping to delete a draft The swipe-to-delete gesture on the drafts screen was too sensitive, causing accidental deletions. Added a confirmation dialog that appears after the swipe gesture completes, requiring the user to confirm before the draft is actually deleted. https://claude.ai/code/session_01JWJxejv2EKrPj2KGEfPPxU --- .../amethyst/ui/components/SwipeToDelete.kt | 64 +++++++++++++++++++ .../screen/loggedIn/drafts/DraftListScreen.kt | 4 +- amethyst/src/main/res/values/strings.xml | 1 + 3 files changed, 67 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt index 9f28df570f..e64d2534bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -38,10 +39,15 @@ import androidx.compose.material3.SwipeToDismissBoxState import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart import androidx.compose.material3.SwipeToDismissBoxValue.Settled import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -88,6 +94,64 @@ fun SwipeToDeleteContainer( ) } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SwipeToDeleteWithConfirmation( + modifier: Modifier = Modifier, + onStartToEnd: () -> Unit, + content: @Composable (RowScope.() -> Unit), +) { + var showConfirmDialog by remember { mutableStateOf(false) } + + val dismissState = + rememberSwipeToDismissBoxState( + confirmValueChange = { + when (it) { + StartToEnd -> { + showConfirmDialog = true + } + EndToStart -> {} + Settled -> {} + } + return@rememberSwipeToDismissBoxState false + }, + positionalThreshold = { it * .40f }, + ) + + if (showConfirmDialog) { + AlertDialog( + onDismissRequest = { showConfirmDialog = false }, + title = { Text(text = stringRes(id = R.string.request_deletion)) }, + text = { Text(text = stringRes(id = R.string.delete_draft_confirmation)) }, + confirmButton = { + TextButton( + onClick = { + showConfirmDialog = false + onStartToEnd() + }, + ) { + Text(text = stringRes(id = R.string.yes)) + } + }, + dismissButton = { + TextButton( + onClick = { showConfirmDialog = false }, + ) { + Text(text = stringRes(id = R.string.no)) + } + }, + ) + } + + SwipeToDismissBox( + state = dismissState, + modifier = modifier, + backgroundContent = { DismissBackground(dismissState) }, + enableDismissFromEndToStart = false, + content = content, + ) +} + @Composable fun DismissBackground(dismissState: SwipeToDismissBoxState) { val color by animateColorAsState( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index 110ba86150..fcc2f3eb6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -48,7 +48,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState -import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteContainer +import com.vitorpamplona.amethyst.ui.components.SwipeToDeleteWithConfirmation import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys.DRAFTS @@ -168,7 +168,7 @@ private fun DraftFeedLoaded( } itemsIndexed(items.list, key = { _, item -> item.idHex }) { _, item -> Row(Modifier.fillMaxWidth().animateItem()) { - SwipeToDeleteContainer( + SwipeToDeleteWithConfirmation( modifier = Modifier.fillMaxWidth().animateContentSize(), onStartToEnd = { accountViewModel.delete(item) }, ) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 72f3724dc7..8a710fd466 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1473,6 +1473,7 @@ Add a Blossom Server Delete all Are you sure you want to delete all drafts? + Are you sure you want to delete this draft? " +%1$s" Stack: From 82e500eee1546917e7382d29269842be0e73c932 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 03:06:22 +0000 Subject: [PATCH 041/172] feat: complete NIP-38 implementation with p tag, emoji, and expiration support - Add optional r/p/e/a/emoji tags to StatusEvent.create() for full spec compliance - Add GENERAL and MUSIC type constants to StatusEvent - Filter expired statuses in UserStatusCache.addStatus() and add removeExpired() - Add p tag (profile reference) display in DisplayStatusInner - Add NIP-30 custom emoji rendering in status display via CreateTextWithEmoji - Allow status type parameter in UserStatusAction.create() https://claude.ai/code/session_0117asp4mDiR65dxbAGKi2hg --- .../ui/note/NIP05VerificationDisplay.kt | 66 ++++++++++++++++--- .../nip38UserStatuses/UserStatusAction.kt | 3 +- .../nip38UserStatuses/UserStatusCache.kt | 15 +++++ .../quartz/nip38UserStatus/StatusEvent.kt | 21 +++++- 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index eaee4475ca..7e15935435 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -59,9 +59,12 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.components.ClickableTextPrimary +import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.LoadNote import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor +import com.vitorpamplona.amethyst.ui.navigation.routes.routeForUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -77,8 +80,13 @@ import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.firstTaggedAddress import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent +import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUserId +import com.vitorpamplona.quartz.nip30CustomEmoji.taggedEmojis import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.toImmutableMap import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.seconds @@ -220,12 +228,24 @@ fun DisplayStatus( accountViewModel: AccountViewModel, nav: INav, ) { + val emojis = + remember(event) { + val emojiList = event.taggedEmojis() + if (emojiList.isEmpty()) { + persistentMapOf() + } else { + emojiList.associate { it.code to it.url }.toImmutableMap() + } + } + DisplayStatusInner( event.content, event.dTag(), event.firstTaggedUrl()?.ifBlank { null }, event.firstTaggedAddress(), event.firstTaggedEvent(), + event.firstTaggedUserId(), + emojis, accountViewModel, nav, ) @@ -238,11 +258,13 @@ fun DisplayStatusInner( url: String?, nostrATag: Address?, nostrETag: ETag?, + nostrPTag: String?, + emojis: ImmutableMap, accountViewModel: AccountViewModel, nav: INav, ) { when (type) { - "music" -> { + StatusEvent.MUSIC -> { Icon( imageVector = CustomHashTagIcons.Tunestr, null, @@ -254,13 +276,24 @@ fun DisplayStatusInner( else -> {} } - Text( - text = content, - fontSize = Font14SP, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + if (emojis.isNotEmpty()) { + CreateTextWithEmoji( + text = content, + emojis = emojis, + color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font14SP, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else { + Text( + text = content, + fontSize = Font14SP, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } if (url != null) { val uri = LocalUriHandler.current @@ -320,6 +353,23 @@ fun DisplayStatusInner( } } } + } else if (nostrPTag != null) { + LoadUser(baseUserHex = nostrPTag, accountViewModel) { user -> + if (user != null) { + Spacer(modifier = StdHorzSpacer) + IconButton( + modifier = Size15Modifier, + onClick = { nav.nav(routeForUser(nostrPTag)) }, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + null, + modifier = Size15Modifier, + tint = MaterialTheme.colorScheme.lessImportantLink, + ) + } + } + } } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt index 1d0acdac71..0040e9e0d8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt @@ -31,7 +31,8 @@ class UserStatusAction { suspend fun create( newStatus: String, signer: NostrSigner, - ): StatusEvent = StatusEvent.create(newStatus, "general", expiration = null, signer) + type: String = StatusEvent.GENERAL, + ): StatusEvent = StatusEvent.create(newStatus, type, signer = signer) suspend fun update( oldStatus: AddressableNote, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt index 477653eb5b..2298cc18ed 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.UserDependencies import com.vitorpamplona.quartz.nip40Expiration.expiration +import com.vitorpamplona.quartz.nip40Expiration.isExpired import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -43,6 +44,9 @@ class UserStatusCache : UserDependencies { // if it's already there, quick exit if (statuses.value.contains(note) || note.event?.content.isNullOrBlank()) return + // don't add expired statuses + if (note.event?.isExpired() == true) return + statuses.update { (it + note).sortedWith(sortModel).toImmutableList() } @@ -56,4 +60,15 @@ class UserStatusCache : UserDependencies { (it - deleteNote).toImmutableList() } } + + fun removeExpired() { + statuses.update { list -> + val filtered = list.filter { it.event?.isExpired() != true } + if (filtered.size != list.size) { + filtered.toImmutableList() + } else { + list + } + } + } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt index 28485f34db..cc3a80b17f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip38UserStatus/StatusEvent.kt @@ -25,6 +25,10 @@ import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.firstTagValue import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip01Core.tags.people.PTag +import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag import com.vitorpamplona.quartz.utils.TimeUtils @Immutable @@ -41,10 +45,18 @@ class StatusEvent( companion object { const val KIND = 30315 + const val GENERAL = "general" + const val MUSIC = "music" + suspend fun create( msg: String, - type: String, - expiration: Long?, + type: String = GENERAL, + expiration: Long? = null, + url: String? = null, + profileId: HexKey? = null, + eventId: HexKey? = null, + addressableId: String? = null, + emojiTags: List? = null, signer: NostrSigner, createdAt: Long = TimeUtils.now(), ): StatusEvent { @@ -52,6 +64,11 @@ class StatusEvent( tags.add(arrayOf("d", type)) expiration?.let { tags.add(arrayOf("expiration", it.toString())) } + url?.let { tags.add(arrayOf("r", it)) } + profileId?.let { tags.add(PTag.assemble(it, null)) } + eventId?.let { tags.add(ETag.assemble(it, null, null)) } + addressableId?.let { tags.add(ATag.assemble(it, null)) } + emojiTags?.forEach { tags.add(it.toTagArray()) } return signer.sign(createdAt, KIND, tags.toTypedArray(), msg) } From da26ab14ab2b65c7842bde86f23f1d61d1be10d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 03:15:21 +0000 Subject: [PATCH 042/172] feat: complete NIP-47 Nostr Wallet Connect implementation in Quartz Implements the full NIP-47 specification with all request methods, response types, notification events, and JSON serialization. New request methods: - pay_invoice (updated with amount, metadata params) - pay_keysend (amount, pubkey, preimage, TLV records) - make_invoice, lookup_invoice, list_transactions - get_balance, get_info - make_hold_invoice, cancel_hold_invoice, settle_hold_invoice New response types: - Success responses for all methods above - NwcErrorResponse (generic error for any method) - NwcErrorCode enum with UNSUPPORTED_ENCRYPTION New event kinds: - NwcInfoEvent (kind 13194) - wallet service capabilities - NwcNotificationEvent (kind 23197) - NIP-44 encrypted notifications - Legacy kind 23196 support for NIP-04 notifications New data models: - NwcTransaction (shared invoice/payment object) - TlvRecord (for keysend TLV records) - Notification types: payment_received, payment_sent, hold_invoice_accepted - HoldInvoiceAcceptedData with settle_deadline Tags (following NIP-88 pattern): - EncryptionTag for encryption scheme negotiation - NotificationsTag for supported notification types Updated: - LnZapPaymentRequestEvent: generic createRequest() with NIP-44 support - URI parser: lud16 parameter support - Jackson deserializers: all methods routed correctly - EventFactory: new kinds registered https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../LnZapPaymentRequestEvent.kt | 38 +++- .../nip47WalletConnect/Nip47WalletConnect.kt | 11 +- .../quartz/nip47WalletConnect/Notification.kt | 59 +++++++ .../quartz/nip47WalletConnect/NwcErrorCode.kt | 39 +++++ .../quartz/nip47WalletConnect/NwcInfoEvent.kt | 69 ++++++++ .../quartz/nip47WalletConnect/NwcMethod.kt | 34 ++++ .../NwcNotificationEvent.kt | 58 +++++++ .../nip47WalletConnect/NwcTransaction.kt | 42 +++++ .../quartz/nip47WalletConnect/Request.kt | 163 +++++++++++++++++- .../quartz/nip47WalletConnect/Response.kt | 112 ++++++++---- .../nip47WalletConnect/tags/EncryptionTag.kt | 41 +++++ .../tags/NotificationsTag.kt | 41 +++++ .../quartz/utils/EventFactory.kt | 5 + .../quartz/nip01Core/jackson/JacksonMapper.kt | 3 + .../jackson/NotificationDeserializer.kt | 49 ++++++ .../jackson/RequestDeserializer.kt | 25 ++- .../jackson/ResponseDeserializer.kt | 60 +++++-- 17 files changed, 784 insertions(+), 65 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Notification.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcErrorCode.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethod.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/tags/EncryptionTag.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/tags/NotificationsTag.kt create mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/NotificationDeserializer.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt index 2a766ef0b5..452c99b2f4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEvent.kt @@ -52,25 +52,47 @@ class LnZapPaymentRequestEvent( return OptimizedJsonMapper.fromJsonTo(jsonText) } + fun encryptionScheme() = tags.firstOrNull { it.size > 1 && it[0] == "encryption" }?.get(1) + companion object { const val KIND = 23194 - const val ALT = "Zap payment request" + const val ALT = "NWC request" suspend fun create( lnInvoice: String, walletServicePubkey: String, signer: NostrSigner, createdAt: Long = TimeUtils.now(), - ): LnZapPaymentRequestEvent { - val serializedRequest = OptimizedJsonMapper.toJson(PayInvoiceMethod.create(lnInvoice)) + ): LnZapPaymentRequestEvent = + createRequest( + PayInvoiceMethod.create(lnInvoice), + walletServicePubkey, + signer, + createdAt, + ) - val tags = arrayOf(arrayOf("p", walletServicePubkey), AltTag.assemble(ALT)) + suspend fun createRequest( + request: Request, + walletServicePubkey: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + useNip44: Boolean = false, + ): LnZapPaymentRequestEvent { + val serializedRequest = OptimizedJsonMapper.toJson(request) + + val tags = + if (useNip44) { + arrayOf(arrayOf("p", walletServicePubkey), AltTag.assemble(ALT), arrayOf("encryption", "nip44_v2")) + } else { + arrayOf(arrayOf("p", walletServicePubkey), AltTag.assemble(ALT)) + } val encrypted = - signer.nip04Encrypt( - serializedRequest, - walletServicePubkey, - ) + if (useNip44) { + signer.nip44Encrypt(serializedRequest, walletServicePubkey) + } else { + signer.nip04Encrypt(serializedRequest, walletServicePubkey) + } return signer.sign(createdAt, KIND, tags, encrypted) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt index e3694b02ab..53eb83d7f6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnect.kt @@ -30,11 +30,10 @@ import com.vitorpamplona.quartz.utils.UriParser import kotlinx.coroutines.CancellationException import kotlinx.serialization.Serializable -// Rename to the corect nip number when ready. class Nip47WalletConnect { companion object { fun parse(uri: String): Nip47URINorm { - // nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&metadata=%7B%22name%22%3A%22Example%22%7D + // nostr+walletconnect://b889ff5b...?relay=wss%3A%2F%2Frelay.damus.io&secret=...&lud16=user@example.com val url = UriParser(uri) @@ -55,8 +54,9 @@ class Nip47WalletConnect { val relay = url.getQueryParameter("relay") ?: throw IllegalArgumentException("Relay cannot be null") val relayNorm = RelayUrlNormalizer.normalizeOrNull(relay) ?: throw IllegalArgumentException("Invalid relay Url") val secret = url.getQueryParameter("secret") + val lud16 = url.getQueryParameter("lud16") - return Nip47URINorm(pubkeyHex, relayNorm, secret) + return Nip47URINorm(pubkeyHex, relayNorm, secret, lud16) } } @@ -65,6 +65,7 @@ class Nip47WalletConnect { val pubKeyHex: HexKey, val relayUri: String, val secret: HexKey?, + val lud16: String? = null, ) { fun normalize(): Nip47URINorm? = RelayUrlNormalizer.normalizeOrNull(relayUri)?.let { @@ -72,6 +73,7 @@ class Nip47WalletConnect { pubKeyHex, it, secret, + lud16, ) } @@ -86,7 +88,8 @@ class Nip47WalletConnect { val pubKeyHex: HexKey, val relayUri: NormalizedRelayUrl, val secret: HexKey?, + val lud16: String? = null, ) { - fun denormalize(): Nip47URI? = Nip47URI(pubKeyHex, relayUri.url, secret) + fun denormalize(): Nip47URI? = Nip47URI(pubKeyHex, relayUri.url, secret, lud16) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Notification.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Notification.kt new file mode 100644 index 0000000000..2c1bbdfc36 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Notification.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable + +object NwcNotificationType { + const val PAYMENT_RECEIVED = "payment_received" + const val PAYMENT_SENT = "payment_sent" + const val HOLD_INVOICE_ACCEPTED = "hold_invoice_accepted" +} + +// NOTIFICATION OBJECTS +abstract class Notification( + val notification_type: String, +) : OptimizedSerializable + +// payment_received notification +class PaymentReceivedNotification( + val notification: NwcTransaction? = null, +) : Notification(NwcNotificationType.PAYMENT_RECEIVED) + +// payment_sent notification +class PaymentSentNotification( + val notification: NwcTransaction? = null, +) : Notification(NwcNotificationType.PAYMENT_SENT) + +// hold_invoice_accepted notification +class HoldInvoiceAcceptedNotification( + val notification: HoldInvoiceAcceptedData? = null, +) : Notification(NwcNotificationType.HOLD_INVOICE_ACCEPTED) + +class HoldInvoiceAcceptedData( + var type: String? = null, + var invoice: String? = null, + var payment_hash: String? = null, + var amount: Long? = null, + var created_at: Long? = null, + var expires_at: Long? = null, + var settle_deadline: Long? = null, +) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcErrorCode.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcErrorCode.kt new file mode 100644 index 0000000000..f88fd18924 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcErrorCode.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +enum class NwcErrorCode { + RATE_LIMITED, + NOT_IMPLEMENTED, + INSUFFICIENT_BALANCE, + PAYMENT_FAILED, + QUOTA_EXCEEDED, + RESTRICTED, + UNAUTHORIZED, + INTERNAL, + UNSUPPORTED_ENCRYPTION, + OTHER, +} + +class NwcError( + var code: NwcErrorCode? = null, + var message: String? = null, +) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEvent.kt new file mode 100644 index 0000000000..42ca7529a1 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEvent.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip47WalletConnect.tags.EncryptionTag +import com.vitorpamplona.quartz.nip47WalletConnect.tags.NotificationsTag +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class NwcInfoEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + fun capabilities(): List = content.split(" ").filter { it.isNotBlank() } + + fun supportsMethod(method: String): Boolean = capabilities().contains(method) + + fun supportsNotifications(): Boolean = capabilities().contains("notifications") + + fun encryptionSchemes() = tags.mapNotNull(EncryptionTag::parse).flatten() + + fun notificationTypes() = tags.mapNotNull(NotificationsTag::parse).flatten() + + companion object { + const val KIND = 13194 + const val ALT_DESCRIPTION = "Wallet service info" + + fun build( + capabilities: List, + encryptionSchemes: List? = null, + notificationTypes: List? = null, + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, capabilities.joinToString(" "), createdAt) { + alt(ALT_DESCRIPTION) + encryptionSchemes?.let { addUnique(EncryptionTag.assemble(it)) } + notificationTypes?.let { addUnique(NotificationsTag.assemble(it)) } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethod.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethod.kt new file mode 100644 index 0000000000..e6195639a8 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethod.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +object NwcMethod { + const val PAY_INVOICE = "pay_invoice" + const val PAY_KEYSEND = "pay_keysend" + const val MAKE_INVOICE = "make_invoice" + const val LOOKUP_INVOICE = "lookup_invoice" + const val LIST_TRANSACTIONS = "list_transactions" + const val GET_BALANCE = "get_balance" + const val GET_INFO = "get_info" + const val MAKE_HOLD_INVOICE = "make_hold_invoice" + const val CANCEL_HOLD_INVOICE = "cancel_hold_invoice" + const val SETTLE_HOLD_INVOICE = "settle_hold_invoice" +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt new file mode 100644 index 0000000000..77b16a3ec6 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions + +@Immutable +class NwcNotificationEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + override fun isContentEncoded() = true + + fun clientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) + + fun talkingWith(oneSideHex: String): HexKey = if (pubKey == oneSideHex) clientPubKey() ?: pubKey else pubKey + + fun canDecrypt(signer: NostrSigner) = pubKey == signer.pubKey || clientPubKey() == signer.pubKey + + suspend fun decryptNotification(signer: NostrSigner): Notification { + if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException() + val jsonText = signer.nip44Decrypt(content, talkingWith(signer.pubKey)) + return OptimizedJsonMapper.fromJsonTo(jsonText) + } + + companion object { + const val KIND = 23197 + const val LEGACY_KIND = 23196 + const val ALT = "Wallet notification" + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt new file mode 100644 index 0000000000..d2a043f59c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +class NwcTransaction( + var type: String? = null, + var state: String? = null, + var invoice: String? = null, + var description: String? = null, + var description_hash: String? = null, + var preimage: String? = null, + var payment_hash: String? = null, + var amount: Long? = null, + var fees_paid: Long? = null, + var created_at: Long? = null, + var expires_at: Long? = null, + var settled_at: Long? = null, + var metadata: Any? = null, +) + +class TlvRecord( + var type: Long? = null, + var value: String? = null, +) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt index b53f3482ed..8d0ea548b2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt @@ -27,15 +27,174 @@ abstract class Request( var method: String? = null, ) : OptimizedSerializable -// PayInvoice Call +// pay_invoice class PayInvoiceParams( var invoice: String? = null, + var amount: Long? = null, + var metadata: Any? = null, ) class PayInvoiceMethod( var params: PayInvoiceParams? = null, -) : Request("pay_invoice") { +) : Request(NwcMethod.PAY_INVOICE) { companion object { fun create(bolt11: String): PayInvoiceMethod = PayInvoiceMethod(PayInvoiceParams(bolt11)) + + fun create( + bolt11: String, + amount: Long, + ): PayInvoiceMethod = PayInvoiceMethod(PayInvoiceParams(bolt11, amount)) + } +} + +// pay_keysend +class PayKeysendParams( + var amount: Long? = null, + var pubkey: String? = null, + var preimage: String? = null, + var tlv_records: List? = null, +) + +class PayKeysendMethod( + var params: PayKeysendParams? = null, +) : Request(NwcMethod.PAY_KEYSEND) { + companion object { + fun create( + amount: Long, + pubkey: String, + preimage: String? = null, + tlvRecords: List? = null, + ): PayKeysendMethod = PayKeysendMethod(PayKeysendParams(amount, pubkey, preimage, tlvRecords)) + } +} + +// make_invoice +class MakeInvoiceParams( + var amount: Long? = null, + var description: String? = null, + var description_hash: String? = null, + var expiry: Long? = null, + var metadata: Any? = null, +) + +class MakeInvoiceMethod( + var params: MakeInvoiceParams? = null, +) : Request(NwcMethod.MAKE_INVOICE) { + companion object { + fun create( + amount: Long, + description: String? = null, + descriptionHash: String? = null, + expiry: Long? = null, + ): MakeInvoiceMethod = MakeInvoiceMethod(MakeInvoiceParams(amount, description, descriptionHash, expiry)) + } +} + +// lookup_invoice +class LookupInvoiceParams( + var payment_hash: String? = null, + var invoice: String? = null, +) + +class LookupInvoiceMethod( + var params: LookupInvoiceParams? = null, +) : Request(NwcMethod.LOOKUP_INVOICE) { + companion object { + fun createByHash(paymentHash: String): LookupInvoiceMethod = LookupInvoiceMethod(LookupInvoiceParams(payment_hash = paymentHash)) + + fun createByInvoice(invoice: String): LookupInvoiceMethod = LookupInvoiceMethod(LookupInvoiceParams(invoice = invoice)) + } +} + +// list_transactions +class ListTransactionsParams( + var from: Long? = null, + var until: Long? = null, + var limit: Int? = null, + var offset: Int? = null, + var unpaid: Boolean? = null, + var type: String? = null, +) + +class ListTransactionsMethod( + var params: ListTransactionsParams? = null, +) : Request(NwcMethod.LIST_TRANSACTIONS) { + companion object { + fun create( + from: Long? = null, + until: Long? = null, + limit: Int? = null, + offset: Int? = null, + unpaid: Boolean? = null, + type: String? = null, + ): ListTransactionsMethod = ListTransactionsMethod(ListTransactionsParams(from, until, limit, offset, unpaid, type)) + } +} + +// get_balance +class GetBalanceMethod : Request(NwcMethod.GET_BALANCE) { + companion object { + fun create(): GetBalanceMethod = GetBalanceMethod() + } +} + +// get_info +class GetInfoMethod : Request(NwcMethod.GET_INFO) { + companion object { + fun create(): GetInfoMethod = GetInfoMethod() + } +} + +// make_hold_invoice +class MakeHoldInvoiceParams( + var amount: Long? = null, + var description: String? = null, + var description_hash: String? = null, + var expiry: Long? = null, + var payment_hash: String? = null, + var min_cltv_expiry_delta: Int? = null, +) + +class MakeHoldInvoiceMethod( + var params: MakeHoldInvoiceParams? = null, +) : Request(NwcMethod.MAKE_HOLD_INVOICE) { + companion object { + fun create( + amount: Long, + paymentHash: String, + description: String? = null, + descriptionHash: String? = null, + expiry: Long? = null, + minCltvExpiryDelta: Int? = null, + ): MakeHoldInvoiceMethod = + MakeHoldInvoiceMethod( + MakeHoldInvoiceParams(amount, description, descriptionHash, expiry, paymentHash, minCltvExpiryDelta), + ) + } +} + +// cancel_hold_invoice +class CancelHoldInvoiceParams( + var payment_hash: String? = null, +) + +class CancelHoldInvoiceMethod( + var params: CancelHoldInvoiceParams? = null, +) : Request(NwcMethod.CANCEL_HOLD_INVOICE) { + companion object { + fun create(paymentHash: String): CancelHoldInvoiceMethod = CancelHoldInvoiceMethod(CancelHoldInvoiceParams(paymentHash)) + } +} + +// settle_hold_invoice +class SettleHoldInvoiceParams( + var preimage: String? = null, +) + +class SettleHoldInvoiceMethod( + var params: SettleHoldInvoiceParams? = null, +) : Request(NwcMethod.SETTLE_HOLD_INVOICE) { + companion object { + fun create(preimage: String): SettleHoldInvoiceMethod = SettleHoldInvoiceMethod(SettleHoldInvoiceParams(preimage)) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt index cb449136af..5c4d3d6a02 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt @@ -27,49 +27,97 @@ abstract class Response( val resultType: String, ) : OptimizedSerializable -// PayInvoice Call +// Generic error response for any method +class NwcErrorResponse( + resultType: String, + val error: NwcError? = null, +) : Response(resultType) +// pay_invoice success response class PayInvoiceSuccessResponse( val result: PayInvoiceResultParams? = null, -) : Response("pay_invoice") { +) : Response(NwcMethod.PAY_INVOICE) { class PayInvoiceResultParams( val preimage: String? = null, + val fees_paid: Long? = null, ) } +// pay_invoice error response (kept for backward compatibility) class PayInvoiceErrorResponse( val error: PayInvoiceErrorParams? = null, -) : Response("pay_invoice") { +) : Response(NwcMethod.PAY_INVOICE) { class PayInvoiceErrorParams( - val code: ErrorType? = null, + val code: NwcErrorCode? = null, val message: String? = null, ) - - enum class ErrorType { - RATE_LIMITED, - - // The client is sending commands too fast. It should retry in a few seconds. - NOT_IMPLEMENTED, - - // The command is not known or is intentionally not implemented. - INSUFFICIENT_BALANCE, - - // The command is not known or is intentionally not implemented. - PAYMENT_FAILED, - - // The wallet does not have enough funds to cover a fee reserve or the payment amount. - QUOTA_EXCEEDED, - - // The wallet has exceeded its spending quota. - RESTRICTED, - - // This public key is not allowed to do this operation. - UNAUTHORIZED, - - // This public key has no wallet connected. - INTERNAL, - - // An internal error. - OTHER, // Other error. - } } + +// pay_keysend success response +class PayKeysendSuccessResponse( + val result: PayKeysendResult? = null, +) : Response(NwcMethod.PAY_KEYSEND) { + class PayKeysendResult( + val preimage: String? = null, + val fees_paid: Long? = null, + ) +} + +// make_invoice success response +class MakeInvoiceSuccessResponse( + val result: NwcTransaction? = null, +) : Response(NwcMethod.MAKE_INVOICE) + +// lookup_invoice success response +class LookupInvoiceSuccessResponse( + val result: NwcTransaction? = null, +) : Response(NwcMethod.LOOKUP_INVOICE) + +// list_transactions success response +class ListTransactionsSuccessResponse( + val result: ListTransactionsResult? = null, +) : Response(NwcMethod.LIST_TRANSACTIONS) { + class ListTransactionsResult( + val transactions: List? = null, + ) +} + +// get_balance success response +class GetBalanceSuccessResponse( + val result: GetBalanceResult? = null, +) : Response(NwcMethod.GET_BALANCE) { + class GetBalanceResult( + val balance: Long? = null, + ) +} + +// get_info success response +class GetInfoSuccessResponse( + val result: GetInfoResult? = null, +) : Response(NwcMethod.GET_INFO) { + class GetInfoResult( + val alias: String? = null, + val color: String? = null, + val pubkey: String? = null, + val network: String? = null, + val block_height: Long? = null, + val block_hash: String? = null, + val methods: List? = null, + val notifications: List? = null, + ) +} + +// make_hold_invoice success response +class MakeHoldInvoiceSuccessResponse( + val result: NwcTransaction? = null, +) : Response(NwcMethod.MAKE_HOLD_INVOICE) + +// cancel_hold_invoice success response +class CancelHoldInvoiceSuccessResponse( + val result: Any? = null, +) : Response(NwcMethod.CANCEL_HOLD_INVOICE) + +// settle_hold_invoice success response +class SettleHoldInvoiceSuccessResponse( + val result: Any? = null, +) : Response(NwcMethod.SETTLE_HOLD_INVOICE) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/tags/EncryptionTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/tags/EncryptionTag.kt new file mode 100644 index 0000000000..3d732528fe --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/tags/EncryptionTag.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class EncryptionTag { + companion object { + const val TAG_NAME = "encryption" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): List? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag.drop(1) + } + + fun assemble(schemes: List) = arrayOf(TAG_NAME, *schemes.toTypedArray()) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/tags/NotificationsTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/tags/NotificationsTag.kt new file mode 100644 index 0000000000..cb42cce542 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/tags/NotificationsTag.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect.tags + +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure + +class NotificationsTag { + companion object { + const val TAG_NAME = "notifications" + + fun isTag(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty() + + fun parse(tag: Array): List? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag.drop(1) + } + + fun assemble(types: List) = arrayOf(TAG_NAME, *types.toTypedArray()) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index eee5e86891..4679e46654 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -78,6 +78,8 @@ import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent +import com.vitorpamplona.quartz.nip47WalletConnect.NwcInfoEvent +import com.vitorpamplona.quartz.nip47WalletConnect.NwcNotificationEvent import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent @@ -256,6 +258,9 @@ class EventFactory { LnZapEvent.KIND -> LnZapEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentRequestEvent.KIND -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig) LnZapPaymentResponseEvent.KIND -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig) + NwcInfoEvent.KIND -> NwcInfoEvent(id, pubKey, createdAt, tags, content, sig) + NwcNotificationEvent.KIND -> NwcNotificationEvent(id, pubKey, createdAt, tags, content, sig) + NwcNotificationEvent.LEGACY_KIND -> NwcNotificationEvent(id, pubKey, createdAt, tags, content, sig) LnZapPrivateEvent.KIND -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig) LnZapRequestEvent.KIND -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) LongTextNoteEvent.KIND -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt index bec5f11b4b..7729b4dd86 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt @@ -52,8 +52,10 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerRequestDeseriali import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerRequestSerializer import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerResponseDeserializer import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerResponseSerializer +import com.vitorpamplona.quartz.nip47WalletConnect.Notification import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.Response +import com.vitorpamplona.quartz.nip47WalletConnect.jackson.NotificationDeserializer import com.vitorpamplona.quartz.nip47WalletConnect.jackson.RequestDeserializer import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor @@ -93,6 +95,7 @@ class JacksonMapper { // nip 47 .addDeserializer(Response::class.java, ResponseDeserializer()) .addDeserializer(Request::class.java, RequestDeserializer()) + .addDeserializer(Notification::class.java, NotificationDeserializer()) // nip 46 .addDeserializer(BunkerMessage::class.java, BunkerMessageDeserializer()) .addSerializer(BunkerRequest::class.java, BunkerRequestSerializer()) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/NotificationDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/NotificationDeserializer.kt new file mode 100644 index 0000000000..6f3d2ccabb --- /dev/null +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/NotificationDeserializer.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect.jackson + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedNotification +import com.vitorpamplona.quartz.nip47WalletConnect.Notification +import com.vitorpamplona.quartz.nip47WalletConnect.NwcNotificationType +import com.vitorpamplona.quartz.nip47WalletConnect.PaymentReceivedNotification +import com.vitorpamplona.quartz.nip47WalletConnect.PaymentSentNotification +import com.vitorpamplona.quartz.utils.asTextOrNull + +class NotificationDeserializer : StdDeserializer(Notification::class.java) { + override fun deserialize( + jp: JsonParser, + ctxt: DeserializationContext, + ): Notification? { + val jsonObject: JsonNode = jp.codec.readTree(jp) + val notificationType = jsonObject.get("notification_type")?.asTextOrNull() + + return when (notificationType) { + NwcNotificationType.PAYMENT_RECEIVED -> jp.codec.treeToValue(jsonObject, PaymentReceivedNotification::class.java) + NwcNotificationType.PAYMENT_SENT -> jp.codec.treeToValue(jsonObject, PaymentSentNotification::class.java) + NwcNotificationType.HOLD_INVOICE_ACCEPTED -> jp.codec.treeToValue(jsonObject, HoldInvoiceAcceptedNotification::class.java) + else -> null + } + } +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestDeserializer.kt index 8e0c5c1778..4098a5dc94 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestDeserializer.kt @@ -24,8 +24,18 @@ import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoMethod +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod +import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendMethod import com.vitorpamplona.quartz.nip47WalletConnect.Request +import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceMethod import com.vitorpamplona.quartz.utils.asTextOrNull class RequestDeserializer : StdDeserializer(Request::class.java) { @@ -36,9 +46,18 @@ class RequestDeserializer : StdDeserializer(Request::class.java) { val jsonObject: JsonNode = jp.codec.readTree(jp) val method = jsonObject.get("method")?.asTextOrNull() - if (method == "pay_invoice") { - return jp.codec.treeToValue(jsonObject, PayInvoiceMethod::class.java) + return when (method) { + NwcMethod.PAY_INVOICE -> jp.codec.treeToValue(jsonObject, PayInvoiceMethod::class.java) + NwcMethod.PAY_KEYSEND -> jp.codec.treeToValue(jsonObject, PayKeysendMethod::class.java) + NwcMethod.MAKE_INVOICE -> jp.codec.treeToValue(jsonObject, MakeInvoiceMethod::class.java) + NwcMethod.LOOKUP_INVOICE -> jp.codec.treeToValue(jsonObject, LookupInvoiceMethod::class.java) + NwcMethod.LIST_TRANSACTIONS -> jp.codec.treeToValue(jsonObject, ListTransactionsMethod::class.java) + NwcMethod.GET_BALANCE -> jp.codec.treeToValue(jsonObject, GetBalanceMethod::class.java) + NwcMethod.GET_INFO -> jp.codec.treeToValue(jsonObject, GetInfoMethod::class.java) + NwcMethod.MAKE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, MakeHoldInvoiceMethod::class.java) + NwcMethod.CANCEL_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, CancelHoldInvoiceMethod::class.java) + NwcMethod.SETTLE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, SettleHoldInvoiceMethod::class.java) + else -> null } - return null } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt index f903f01718..72e7685fb8 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt @@ -24,9 +24,21 @@ import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.NwcError +import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse +import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.Response +import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceSuccessResponse import com.vitorpamplona.quartz.utils.asTextOrNull class ResponseDeserializer : StdDeserializer(Response::class.java) { @@ -36,25 +48,41 @@ class ResponseDeserializer : StdDeserializer(Response::class.java) { ): Response? { val jsonObject: JsonNode = jp.codec.readTree(jp) val resultType = jsonObject.get("result_type")?.asTextOrNull() + val hasError = jsonObject.has("error") && !jsonObject.get("error").isNull + val hasResult = jsonObject.has("result") && !jsonObject.get("result").isNull - if (resultType == "pay_invoice") { - val result = jsonObject.get("result") - val error = jsonObject.get("error") - if (result != null) { - return jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java) - } - if (error != null) { - return jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java) - } - } else { - // tries to guess - if (jsonObject.get("result")?.get("preimage") != null) { - return jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java) - } - if (jsonObject.get("error")?.get("code") != null) { - return jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java) + if (hasError) { + return when (resultType) { + NwcMethod.PAY_INVOICE -> jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java) + else -> { + val error = jp.codec.treeToValue(jsonObject.get("error"), NwcError::class.java) + NwcErrorResponse(resultType ?: "", error) + } } } + + if (hasResult || resultType != null) { + return when (resultType) { + NwcMethod.PAY_INVOICE -> jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java) + NwcMethod.PAY_KEYSEND -> jp.codec.treeToValue(jsonObject, PayKeysendSuccessResponse::class.java) + NwcMethod.MAKE_INVOICE -> jp.codec.treeToValue(jsonObject, MakeInvoiceSuccessResponse::class.java) + NwcMethod.LOOKUP_INVOICE -> jp.codec.treeToValue(jsonObject, LookupInvoiceSuccessResponse::class.java) + NwcMethod.LIST_TRANSACTIONS -> jp.codec.treeToValue(jsonObject, ListTransactionsSuccessResponse::class.java) + NwcMethod.GET_BALANCE -> jp.codec.treeToValue(jsonObject, GetBalanceSuccessResponse::class.java) + NwcMethod.GET_INFO -> jp.codec.treeToValue(jsonObject, GetInfoSuccessResponse::class.java) + NwcMethod.MAKE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, MakeHoldInvoiceSuccessResponse::class.java) + NwcMethod.CANCEL_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, CancelHoldInvoiceSuccessResponse::class.java) + NwcMethod.SETTLE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, SettleHoldInvoiceSuccessResponse::class.java) + else -> { + // tries to guess for backward compatibility + if (jsonObject.get("result")?.get("preimage") != null) { + return jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java) + } + null + } + } + } + return null } } From 8c73dd8f14c359362adffc3dcea3a2107b046921 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 03:45:50 +0000 Subject: [PATCH 043/172] test: add comprehensive test suite for NIP-47 Wallet Connect implementation Covers all 10 request methods (create, serialize, deserialize), all success and error response types, notification deserialization, EncryptionTag and NotificationsTag parse/assemble/isTag, NwcInfoEvent build and capabilities, NwcNotificationEvent structure, LnZapPaymentRequestEvent create/decrypt with NIP-04 and NIP-44, and Nip47WalletConnect URI parsing and serialization. https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../LnZapPaymentRequestEventTest.kt | 207 ++++++++++++ .../Nip47WalletConnectTest.kt | 155 +++++++++ .../nip47WalletConnect/NotificationTest.kt | 97 ++++++ .../nip47WalletConnect/NwcInfoEventTest.kt | 124 ++++++++ .../nip47WalletConnect/NwcMethodTest.kt | 70 ++++ .../NwcNotificationEventTest.kt | 144 +++++++++ .../quartz/nip47WalletConnect/RequestTest.kt | 299 ++++++++++++++++++ .../quartz/nip47WalletConnect/ResponseTest.kt | 268 ++++++++++++++++ .../quartz/nip47WalletConnect/TagsTest.kt | 138 ++++++++ 9 files changed, 1502 insertions(+) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnectTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEventTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/TagsTest.kt diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt new file mode 100644 index 0000000000..bc5af24320 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class LnZapPaymentRequestEventTest { + @Test + fun testEventKind() { + assertEquals(23194, LnZapPaymentRequestEvent.KIND) + } + + @Test + fun testCreatePayInvoiceRequest() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + + val event = + LnZapPaymentRequestEvent.create( + lnInvoice = "lnbc50n1...", + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + createdAt = 1000L, + ) + + assertEquals(23194, event.kind) + assertEquals(walletServicePubkey, event.walletServicePubKey()) + assertTrue(event.isContentEncoded()) + assertNotNull(event.content) + assertTrue(event.content.isNotEmpty()) + } + + @Test + fun testCreateGenericRequest() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + + val request = GetBalanceMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + createdAt = 1000L, + ) + + assertEquals(23194, event.kind) + assertEquals(walletServicePubkey, event.walletServicePubKey()) + // Should not have encryption tag for NIP-04 + assertNull(event.encryptionScheme()) + } + + @Test + fun testCreateRequestWithNip44() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + + val request = GetBalanceMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + createdAt = 1000L, + useNip44 = true, + ) + + assertEquals(23194, event.kind) + assertEquals("nip44_v2", event.encryptionScheme()) + } + + @Test + fun testDecryptPayInvoiceRequest() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletSigner = NostrSignerInternal(walletKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + + val event = + LnZapPaymentRequestEvent.create( + lnInvoice = "lnbc50n1...", + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + ) + + // Wallet service should be able to decrypt + val decrypted = event.decryptRequest(walletSigner) + assertIs(decrypted) + assertEquals("lnbc50n1...", decrypted.params?.invoice) + } + + @Test + fun testDecryptGenericRequest() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletSigner = NostrSignerInternal(walletKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + + val request = MakeInvoiceMethod.create(5000L, "test payment") + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + ) + + val decrypted = event.decryptRequest(walletSigner) + assertIs(decrypted) + assertEquals(5000L, decrypted.params?.amount) + assertEquals("test payment", decrypted.params?.description) + } + + @Test + fun testDecryptNip44Request() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletSigner = NostrSignerInternal(walletKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + + val request = GetInfoMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + useNip44 = true, + ) + + assertEquals("nip44_v2", event.encryptionScheme()) + + // Wallet service should be able to decrypt NIP-44 encrypted request + val decrypted = event.decryptRequest(walletSigner) + assertIs(decrypted) + } + + @Test + fun testCanDecrypt() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val otherKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletSigner = NostrSignerInternal(walletKeyPair) + val otherSigner = NostrSignerInternal(otherKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + + val event = + LnZapPaymentRequestEvent.create( + lnInvoice = "lnbc50n1...", + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + ) + + assertTrue(event.canDecrypt(clientSigner)) + assertTrue(event.canDecrypt(walletSigner)) + assertTrue(!event.canDecrypt(otherSigner)) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnectTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnectTest.kt new file mode 100644 index 0000000000..c31c376d71 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnectTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class Nip47WalletConnectTest { + @Test + fun testParseWalletConnectUri() { + val uri = + "nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100571c5" + val parsed = Nip47WalletConnect.parse(uri) + + assertEquals("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", parsed.pubKeyHex) + assertEquals("wss://relay.damus.io/", parsed.relayUri.url) + assertEquals("71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100571c5", parsed.secret) + assertNull(parsed.lud16) + } + + @Test + fun testParseWalletConnectUriWithLud16() { + val uri = + "nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100571c5&lud16=user%40example.com" + val parsed = Nip47WalletConnect.parse(uri) + + assertEquals("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", parsed.pubKeyHex) + assertNotNull(parsed.lud16) + assertEquals("user@example.com", parsed.lud16) + } + + @Test + fun testParseNostrWalletConnectScheme() { + val uri = + "nostrwalletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=abc" + val parsed = Nip47WalletConnect.parse(uri) + + assertEquals("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", parsed.pubKeyHex) + } + + @Test + fun testParseAmethystWalletConnectScheme() { + val uri = + "amethyst+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=abc" + val parsed = Nip47WalletConnect.parse(uri) + + assertEquals("b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", parsed.pubKeyHex) + } + + @Test + fun testParseWithoutSecret() { + val uri = + "nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io" + val parsed = Nip47WalletConnect.parse(uri) + + assertNull(parsed.secret) + } + + @Test + fun testParseInvalidSchemeThrows() { + val uri = "https://example.com?relay=wss%3A%2F%2Frelay.damus.io" + assertFailsWith { + Nip47WalletConnect.parse(uri) + } + } + + @Test + fun testParseWithoutRelayThrows() { + val uri = "nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4" + assertFailsWith { + Nip47WalletConnect.parse(uri) + } + } + + // --- Nip47URI serialization --- + + @Test + fun testNip47UriSerializationRoundTrip() { + val original = + Nip47WalletConnect.Nip47URI( + pubKeyHex = "b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4", + relayUri = "wss://relay.damus.io", + secret = "abc123", + lud16 = "user@example.com", + ) + + val json = Nip47WalletConnect.Nip47URI.serializer(original) + val deserialized = Nip47WalletConnect.Nip47URI.parser(json) + + assertEquals(original.pubKeyHex, deserialized.pubKeyHex) + assertEquals(original.relayUri, deserialized.relayUri) + assertEquals(original.secret, deserialized.secret) + assertEquals(original.lud16, deserialized.lud16) + } + + @Test + fun testNip47UriSerializationWithNullLud16() { + val original = + Nip47WalletConnect.Nip47URI( + pubKeyHex = "abc123", + relayUri = "wss://relay.damus.io", + secret = "secret", + ) + + val json = Nip47WalletConnect.Nip47URI.serializer(original) + val deserialized = Nip47WalletConnect.Nip47URI.parser(json) + + assertEquals(original.pubKeyHex, deserialized.pubKeyHex) + assertNull(deserialized.lud16) + } + + // --- Normalize/Denormalize --- + + @Test + fun testNormalizeDenormalizeRoundTrip() { + val uri = + Nip47WalletConnect.Nip47URI( + pubKeyHex = "abc123", + relayUri = "wss://relay.damus.io", + secret = "secret", + lud16 = "user@example.com", + ) + + val normalized = uri.normalize() + assertNotNull(normalized) + assertEquals("user@example.com", normalized.lud16) + + val denormalized = normalized.denormalize() + assertNotNull(denormalized) + assertEquals("abc123", denormalized.pubKeyHex) + assertEquals("secret", denormalized.secret) + assertEquals("user@example.com", denormalized.lud16) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt new file mode 100644 index 0000000000..b06d3710ca --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class NotificationTest { + @Test + fun testPaymentReceivedDeserialization() { + val json = + """{"notification_type":"payment_received","notification":{"type":"incoming","invoice":"lnbc50n1...","description":"coffee","preimage":"abc","payment_hash":"hash123","amount":5000,"fees_paid":10,"created_at":1693876497,"expires_at":1694876497,"settled_at":1694876500}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertIs(notification) + assertNotNull(notification.notification) + assertEquals("incoming", notification.notification?.type) + assertEquals("lnbc50n1...", notification.notification?.invoice) + assertEquals("coffee", notification.notification?.description) + assertEquals("abc", notification.notification?.preimage) + assertEquals("hash123", notification.notification?.payment_hash) + assertEquals(5000L, notification.notification?.amount) + assertEquals(10L, notification.notification?.fees_paid) + assertEquals(1693876497L, notification.notification?.created_at) + assertEquals(1694876497L, notification.notification?.expires_at) + assertEquals(1694876500L, notification.notification?.settled_at) + } + + @Test + fun testPaymentSentDeserialization() { + val json = + """{"notification_type":"payment_sent","notification":{"type":"outgoing","invoice":"lnbc100n1...","preimage":"def456","payment_hash":"hash456","amount":10000,"fees_paid":50,"created_at":1693876497,"settled_at":1694876500}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertIs(notification) + assertNotNull(notification.notification) + assertEquals("outgoing", notification.notification?.type) + assertEquals("lnbc100n1...", notification.notification?.invoice) + assertEquals("def456", notification.notification?.preimage) + assertEquals(10000L, notification.notification?.amount) + assertEquals(50L, notification.notification?.fees_paid) + } + + @Test + fun testHoldInvoiceAcceptedDeserialization() { + val json = + """{"notification_type":"hold_invoice_accepted","notification":{"type":"incoming","invoice":"lnbc200n1...","payment_hash":"hash789","amount":20000,"created_at":1693876497,"expires_at":1694876497,"settle_deadline":800000}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertIs(notification) + assertNotNull(notification.notification) + assertEquals("incoming", notification.notification?.type) + assertEquals("lnbc200n1...", notification.notification?.invoice) + assertEquals("hash789", notification.notification?.payment_hash) + assertEquals(20000L, notification.notification?.amount) + assertEquals(800000L, notification.notification?.settle_deadline) + assertEquals(1693876497L, notification.notification?.created_at) + assertEquals(1694876497L, notification.notification?.expires_at) + } + + @Test + fun testPaymentReceivedMinimalFields() { + val json = """{"notification_type":"payment_received","notification":{"type":"incoming","amount":100}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertIs(notification) + assertEquals("incoming", notification.notification?.type) + assertEquals(100L, notification.notification?.amount) + assertNull(notification.notification?.invoice) + assertNull(notification.notification?.preimage) + } + + @Test + fun testUnknownNotificationTypeReturnsNull() { + val json = """{"notification_type":"unknown_type","notification":{}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertNull(notification) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt new file mode 100644 index 0000000000..3a8b15ca8c --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.utils.DeterministicSigner +import com.vitorpamplona.quartz.utils.nsecToKeyPair +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class NwcInfoEventTest { + private val signer = DeterministicSigner("nsec1w4uucmeyyng0kegm7486r23sv4majkmvqsj6eypprq0xttxss55s5mgg9t".nsecToKeyPair()) + + @Test + fun testBuildInfoEvent() { + val capabilities = listOf("pay_invoice", "get_balance", "make_invoice", "notifications") + val template = NwcInfoEvent.build(capabilities) + val event = signer.sign(template) + + assertEquals(NwcInfoEvent.KIND, event.kind) + assertEquals("pay_invoice get_balance make_invoice notifications", event.content) + } + + @Test + fun testCapabilities() { + val capabilities = listOf("pay_invoice", "get_balance", "make_invoice") + val template = NwcInfoEvent.build(capabilities) + val event = signer.sign(template) + + val parsed = event.capabilities() + assertEquals(3, parsed.size) + assertTrue(parsed.contains("pay_invoice")) + assertTrue(parsed.contains("get_balance")) + assertTrue(parsed.contains("make_invoice")) + } + + @Test + fun testSupportsMethod() { + val capabilities = listOf("pay_invoice", "get_balance") + val template = NwcInfoEvent.build(capabilities) + val event = signer.sign(template) + + assertTrue(event.supportsMethod("pay_invoice")) + assertTrue(event.supportsMethod("get_balance")) + assertFalse(event.supportsMethod("make_invoice")) + assertFalse(event.supportsMethod("pay_keysend")) + } + + @Test + fun testSupportsNotifications() { + val capabilities = listOf("pay_invoice", "notifications") + val template = NwcInfoEvent.build(capabilities) + val event = signer.sign(template) + + assertTrue(event.supportsNotifications()) + } + + @Test + fun testDoesNotSupportNotifications() { + val capabilities = listOf("pay_invoice", "get_balance") + val template = NwcInfoEvent.build(capabilities) + val event = signer.sign(template) + + assertFalse(event.supportsNotifications()) + } + + @Test + fun testEncryptionSchemes() { + val capabilities = listOf("pay_invoice") + val template = NwcInfoEvent.build(capabilities, encryptionSchemes = listOf("nip44_v2", "nip04")) + val event = signer.sign(template) + + val schemes = event.encryptionSchemes() + assertEquals(2, schemes.size) + assertTrue(schemes.contains("nip44_v2")) + assertTrue(schemes.contains("nip04")) + } + + @Test + fun testNotificationTypes() { + val capabilities = listOf("pay_invoice", "notifications") + val template = NwcInfoEvent.build(capabilities, notificationTypes = listOf("payment_received", "payment_sent")) + val event = signer.sign(template) + + val types = event.notificationTypes() + assertEquals(2, types.size) + assertTrue(types.contains("payment_received")) + assertTrue(types.contains("payment_sent")) + } + + @Test + fun testInfoEventKind() { + assertEquals(13194, NwcInfoEvent.KIND) + } + + @Test + fun testBuildWithNoOptionalTags() { + val capabilities = listOf("pay_invoice") + val template = NwcInfoEvent.build(capabilities) + val event = signer.sign(template) + + assertTrue(event.encryptionSchemes().isEmpty()) + assertTrue(event.notificationTypes().isEmpty()) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt new file mode 100644 index 0000000000..f088a2dbd5 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import kotlin.test.Test +import kotlin.test.assertEquals + +class NwcMethodTest { + @Test + fun testMethodConstants() { + assertEquals("pay_invoice", NwcMethod.PAY_INVOICE) + assertEquals("pay_keysend", NwcMethod.PAY_KEYSEND) + assertEquals("make_invoice", NwcMethod.MAKE_INVOICE) + assertEquals("lookup_invoice", NwcMethod.LOOKUP_INVOICE) + assertEquals("list_transactions", NwcMethod.LIST_TRANSACTIONS) + assertEquals("get_balance", NwcMethod.GET_BALANCE) + assertEquals("get_info", NwcMethod.GET_INFO) + assertEquals("make_hold_invoice", NwcMethod.MAKE_HOLD_INVOICE) + assertEquals("cancel_hold_invoice", NwcMethod.CANCEL_HOLD_INVOICE) + assertEquals("settle_hold_invoice", NwcMethod.SETTLE_HOLD_INVOICE) + } + + @Test + fun testNotificationTypeConstants() { + assertEquals("payment_received", NwcNotificationType.PAYMENT_RECEIVED) + assertEquals("payment_sent", NwcNotificationType.PAYMENT_SENT) + assertEquals("hold_invoice_accepted", NwcNotificationType.HOLD_INVOICE_ACCEPTED) + } + + @Test + fun testErrorCodeValues() { + val codes = NwcErrorCode.entries + assertEquals(10, codes.size) + assertEquals(NwcErrorCode.RATE_LIMITED, NwcErrorCode.valueOf("RATE_LIMITED")) + assertEquals(NwcErrorCode.NOT_IMPLEMENTED, NwcErrorCode.valueOf("NOT_IMPLEMENTED")) + assertEquals(NwcErrorCode.INSUFFICIENT_BALANCE, NwcErrorCode.valueOf("INSUFFICIENT_BALANCE")) + assertEquals(NwcErrorCode.PAYMENT_FAILED, NwcErrorCode.valueOf("PAYMENT_FAILED")) + assertEquals(NwcErrorCode.QUOTA_EXCEEDED, NwcErrorCode.valueOf("QUOTA_EXCEEDED")) + assertEquals(NwcErrorCode.RESTRICTED, NwcErrorCode.valueOf("RESTRICTED")) + assertEquals(NwcErrorCode.UNAUTHORIZED, NwcErrorCode.valueOf("UNAUTHORIZED")) + assertEquals(NwcErrorCode.INTERNAL, NwcErrorCode.valueOf("INTERNAL")) + assertEquals(NwcErrorCode.UNSUPPORTED_ENCRYPTION, NwcErrorCode.valueOf("UNSUPPORTED_ENCRYPTION")) + assertEquals(NwcErrorCode.OTHER, NwcErrorCode.valueOf("OTHER")) + } + + @Test + fun testNwcError() { + val error = NwcError(NwcErrorCode.UNAUTHORIZED, "not allowed") + assertEquals(NwcErrorCode.UNAUTHORIZED, error.code) + assertEquals("not allowed", error.message) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEventTest.kt new file mode 100644 index 0000000000..3b392eb55d --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEventTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class NwcNotificationEventTest { + @Test + fun testKindConstants() { + assertEquals(23197, NwcNotificationEvent.KIND) + assertEquals(23196, NwcNotificationEvent.LEGACY_KIND) + } + + @Test + fun testIsContentEncoded() { + val event = + NwcNotificationEvent( + id = "a".repeat(64), + pubKey = "b".repeat(64), + createdAt = 1234L, + tags = arrayOf(arrayOf("p", "c".repeat(64))), + content = "encrypted_content", + sig = "d".repeat(128), + ) + assertTrue(event.isContentEncoded()) + } + + @Test + fun testClientPubKey() { + val clientPubKey = "c".repeat(64) + val event = + NwcNotificationEvent( + id = "a".repeat(64), + pubKey = "b".repeat(64), + createdAt = 1234L, + tags = arrayOf(arrayOf("p", clientPubKey)), + content = "encrypted", + sig = "d".repeat(128), + ) + assertEquals(clientPubKey, event.clientPubKey()) + } + + @Test + fun testClientPubKeyMissing() { + val event = + NwcNotificationEvent( + id = "a".repeat(64), + pubKey = "b".repeat(64), + createdAt = 1234L, + tags = emptyArray(), + content = "encrypted", + sig = "d".repeat(128), + ) + assertNull(event.clientPubKey()) + } + + @Test + fun testTalkingWithAsWalletService() { + val walletPubKey = "b".repeat(64) + val clientPubKey = "c".repeat(64) + val event = + NwcNotificationEvent( + id = "a".repeat(64), + pubKey = walletPubKey, + createdAt = 1234L, + tags = arrayOf(arrayOf("p", clientPubKey)), + content = "encrypted", + sig = "d".repeat(128), + ) + // Wallet service asking "who am I talking with?" -> client + assertEquals(clientPubKey, event.talkingWith(walletPubKey)) + } + + @Test + fun testTalkingWithAsClient() { + val walletPubKey = "b".repeat(64) + val clientPubKey = "c".repeat(64) + val event = + NwcNotificationEvent( + id = "a".repeat(64), + pubKey = walletPubKey, + createdAt = 1234L, + tags = arrayOf(arrayOf("p", clientPubKey)), + content = "encrypted", + sig = "d".repeat(128), + ) + // Client asking "who am I talking with?" -> wallet service (pubkey) + assertEquals(walletPubKey, event.talkingWith(clientPubKey)) + } + + @Test + fun testEventKindInFactory() { + val event = + NwcNotificationEvent( + id = "a".repeat(64), + pubKey = "b".repeat(64), + createdAt = 1234L, + tags = emptyArray(), + content = "", + sig = "c".repeat(128), + ) + assertEquals(23197, event.kind) + } + + @Test + fun testCanDecryptReturnsFalseForUnrelatedSigner() { + val walletPubKey = "b".repeat(64) + val clientPubKey = "c".repeat(64) + val event = + NwcNotificationEvent( + id = "a".repeat(64), + pubKey = walletPubKey, + createdAt = 1234L, + tags = arrayOf(arrayOf("p", clientPubKey)), + content = "encrypted", + sig = "d".repeat(128), + ) + // A signer that is neither the wallet nor the client shouldn't be able to decrypt + assertFalse(event.clientPubKey() == "e".repeat(64)) + assertFalse(event.pubKey == "e".repeat(64)) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt new file mode 100644 index 0000000000..630f7addf3 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt @@ -0,0 +1,299 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class RequestTest { + // --- PayInvoice --- + + @Test + fun testPayInvoiceCreate() { + val request = PayInvoiceMethod.create("lnbc50n1...") + assertEquals(NwcMethod.PAY_INVOICE, request.method) + assertEquals("lnbc50n1...", request.params?.invoice) + assertNull(request.params?.amount) + } + + @Test + fun testPayInvoiceCreateWithAmount() { + val request = PayInvoiceMethod.create("lnbc50n1...", 1000L) + assertEquals(NwcMethod.PAY_INVOICE, request.method) + assertEquals("lnbc50n1...", request.params?.invoice) + assertEquals(1000L, request.params?.amount) + } + + @Test + fun testPayInvoiceSerialization() { + val request = PayInvoiceMethod.create("lnbc50n1...") + val json = OptimizedJsonMapper.toJson(request) + assertTrue(json.contains("\"method\":\"pay_invoice\"")) + assertTrue(json.contains("\"invoice\":\"lnbc50n1...\"")) + } + + @Test + fun testPayInvoiceDeserialization() { + val json = """{"method":"pay_invoice","params":{"invoice":"lnbc50n1..."}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("lnbc50n1...", request.params?.invoice) + } + + @Test + fun testPayInvoiceWithAmountDeserialization() { + val json = """{"method":"pay_invoice","params":{"invoice":"lnbc50n1...","amount":1000}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("lnbc50n1...", request.params?.invoice) + assertEquals(1000L, request.params?.amount) + } + + // --- PayKeysend --- + + @Test + fun testPayKeysendCreate() { + val request = PayKeysendMethod.create(1000L, "abcdef1234567890") + assertEquals(NwcMethod.PAY_KEYSEND, request.method) + assertEquals(1000L, request.params?.amount) + assertEquals("abcdef1234567890", request.params?.pubkey) + assertNull(request.params?.preimage) + assertNull(request.params?.tlv_records) + } + + @Test + fun testPayKeysendWithTlvRecords() { + val tlvRecords = listOf(TlvRecord(7629169L, "hex_value")) + val request = PayKeysendMethod.create(1000L, "pubkey123", "preimage123", tlvRecords) + assertEquals(1000L, request.params?.amount) + assertEquals("pubkey123", request.params?.pubkey) + assertEquals("preimage123", request.params?.preimage) + assertNotNull(request.params?.tlv_records) + assertEquals(1, request.params?.tlv_records?.size) + assertEquals(7629169L, request.params?.tlv_records?.first()?.type) + assertEquals("hex_value", request.params?.tlv_records?.first()?.value) + } + + @Test + fun testPayKeysendSerialization() { + val request = PayKeysendMethod.create(1000L, "pubkey123") + val json = OptimizedJsonMapper.toJson(request) + assertTrue(json.contains("\"method\":\"pay_keysend\"")) + assertTrue(json.contains("\"amount\":1000")) + assertTrue(json.contains("\"pubkey\":\"pubkey123\"")) + } + + @Test + fun testPayKeysendDeserialization() { + val json = """{"method":"pay_keysend","params":{"amount":1000,"pubkey":"pubkey123"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals(1000L, request.params?.amount) + assertEquals("pubkey123", request.params?.pubkey) + } + + // --- MakeInvoice --- + + @Test + fun testMakeInvoiceCreate() { + val request = MakeInvoiceMethod.create(5000L, "test payment", null, 3600L) + assertEquals(NwcMethod.MAKE_INVOICE, request.method) + assertEquals(5000L, request.params?.amount) + assertEquals("test payment", request.params?.description) + assertNull(request.params?.description_hash) + assertEquals(3600L, request.params?.expiry) + } + + @Test + fun testMakeInvoiceSerialization() { + val request = MakeInvoiceMethod.create(5000L, "test") + val json = OptimizedJsonMapper.toJson(request) + assertTrue(json.contains("\"method\":\"make_invoice\"")) + assertTrue(json.contains("\"amount\":5000")) + } + + @Test + fun testMakeInvoiceDeserialization() { + val json = """{"method":"make_invoice","params":{"amount":5000,"description":"test","expiry":3600}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals(5000L, request.params?.amount) + assertEquals("test", request.params?.description) + assertEquals(3600L, request.params?.expiry) + } + + // --- LookupInvoice --- + + @Test + fun testLookupInvoiceByHash() { + val request = LookupInvoiceMethod.createByHash("abc123") + assertEquals(NwcMethod.LOOKUP_INVOICE, request.method) + assertEquals("abc123", request.params?.payment_hash) + assertNull(request.params?.invoice) + } + + @Test + fun testLookupInvoiceByInvoice() { + val request = LookupInvoiceMethod.createByInvoice("lnbc50n1...") + assertEquals(NwcMethod.LOOKUP_INVOICE, request.method) + assertNull(request.params?.payment_hash) + assertEquals("lnbc50n1...", request.params?.invoice) + } + + @Test + fun testLookupInvoiceDeserialization() { + val json = """{"method":"lookup_invoice","params":{"payment_hash":"abc123"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("abc123", request.params?.payment_hash) + } + + // --- ListTransactions --- + + @Test + fun testListTransactionsCreate() { + val request = ListTransactionsMethod.create(from = 1000L, until = 2000L, limit = 10, offset = 0, unpaid = false, type = "incoming") + assertEquals(NwcMethod.LIST_TRANSACTIONS, request.method) + assertEquals(1000L, request.params?.from) + assertEquals(2000L, request.params?.until) + assertEquals(10, request.params?.limit) + assertEquals(0, request.params?.offset) + assertEquals(false, request.params?.unpaid) + assertEquals("incoming", request.params?.type) + } + + @Test + fun testListTransactionsDeserialization() { + val json = """{"method":"list_transactions","params":{"from":1000,"until":2000,"limit":10}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals(1000L, request.params?.from) + assertEquals(2000L, request.params?.until) + assertEquals(10, request.params?.limit) + } + + // --- GetBalance --- + + @Test + fun testGetBalanceCreate() { + val request = GetBalanceMethod.create() + assertEquals(NwcMethod.GET_BALANCE, request.method) + } + + @Test + fun testGetBalanceSerialization() { + val request = GetBalanceMethod.create() + val json = OptimizedJsonMapper.toJson(request) + assertTrue(json.contains("\"method\":\"get_balance\"")) + } + + @Test + fun testGetBalanceDeserialization() { + val json = """{"method":"get_balance"}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + } + + // --- GetInfo --- + + @Test + fun testGetInfoCreate() { + val request = GetInfoMethod.create() + assertEquals(NwcMethod.GET_INFO, request.method) + } + + @Test + fun testGetInfoDeserialization() { + val json = """{"method":"get_info"}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + } + + // --- MakeHoldInvoice --- + + @Test + fun testMakeHoldInvoiceCreate() { + val request = MakeHoldInvoiceMethod.create(10000L, "payment_hash_abc", "hold invoice", null, 7200L, 144) + assertEquals(NwcMethod.MAKE_HOLD_INVOICE, request.method) + assertEquals(10000L, request.params?.amount) + assertEquals("payment_hash_abc", request.params?.payment_hash) + assertEquals("hold invoice", request.params?.description) + assertEquals(7200L, request.params?.expiry) + assertEquals(144, request.params?.min_cltv_expiry_delta) + } + + @Test + fun testMakeHoldInvoiceDeserialization() { + val json = """{"method":"make_hold_invoice","params":{"amount":10000,"payment_hash":"abc","description":"test"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals(10000L, request.params?.amount) + assertEquals("abc", request.params?.payment_hash) + } + + // --- CancelHoldInvoice --- + + @Test + fun testCancelHoldInvoiceCreate() { + val request = CancelHoldInvoiceMethod.create("payment_hash_abc") + assertEquals(NwcMethod.CANCEL_HOLD_INVOICE, request.method) + assertEquals("payment_hash_abc", request.params?.payment_hash) + } + + @Test + fun testCancelHoldInvoiceDeserialization() { + val json = """{"method":"cancel_hold_invoice","params":{"payment_hash":"abc123"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("abc123", request.params?.payment_hash) + } + + // --- SettleHoldInvoice --- + + @Test + fun testSettleHoldInvoiceCreate() { + val request = SettleHoldInvoiceMethod.create("preimage_xyz") + assertEquals(NwcMethod.SETTLE_HOLD_INVOICE, request.method) + assertEquals("preimage_xyz", request.params?.preimage) + } + + @Test + fun testSettleHoldInvoiceDeserialization() { + val json = """{"method":"settle_hold_invoice","params":{"preimage":"preimage_xyz"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("preimage_xyz", request.params?.preimage) + } + + // --- Unknown method --- + + @Test + fun testUnknownMethodReturnsNull() { + val json = """{"method":"unknown_method","params":{}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertNull(request) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt new file mode 100644 index 0000000000..6bf160d6a4 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class ResponseTest { + // --- PayInvoice Success --- + + @Test + fun testPayInvoiceSuccessDeserialization() { + val json = """{"result_type":"pay_invoice","result":{"preimage":"0123456789abcdef"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("0123456789abcdef", response.result?.preimage) + } + + @Test + fun testPayInvoiceSuccessWithFeesPaid() { + val json = """{"result_type":"pay_invoice","result":{"preimage":"abc","fees_paid":100}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("abc", response.result?.preimage) + assertEquals(100L, response.result?.fees_paid) + } + + @Test + fun testPayInvoiceSuccessGuessWithoutResultType() { + val json = """{"result":{"preimage":"0123456789abcdef"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("0123456789abcdef", response.result?.preimage) + } + + // --- PayInvoice Error --- + + @Test + fun testPayInvoiceErrorDeserialization() { + val json = """{"result_type":"pay_invoice","error":{"code":"INSUFFICIENT_BALANCE","message":"Not enough funds"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.INSUFFICIENT_BALANCE, response.error?.code) + assertEquals("Not enough funds", response.error?.message) + } + + @Test + fun testPayInvoicePaymentFailedError() { + val json = """{"result_type":"pay_invoice","error":{"code":"PAYMENT_FAILED","message":"Route not found"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.PAYMENT_FAILED, response.error?.code) + } + + // --- PayKeysend Success --- + + @Test + fun testPayKeysendSuccessDeserialization() { + val json = """{"result_type":"pay_keysend","result":{"preimage":"abc123","fees_paid":50}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("abc123", response.result?.preimage) + assertEquals(50L, response.result?.fees_paid) + } + + // --- MakeInvoice Success --- + + @Test + fun testMakeInvoiceSuccessDeserialization() { + val json = + """{"result_type":"make_invoice","result":{"type":"incoming","invoice":"lnbc50n1...","description":"test","payment_hash":"abc","amount":5000,"fees_paid":0,"created_at":1693876497,"expires_at":1694876497}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result) + assertEquals("incoming", response.result?.type) + assertEquals("lnbc50n1...", response.result?.invoice) + assertEquals("test", response.result?.description) + assertEquals("abc", response.result?.payment_hash) + assertEquals(5000L, response.result?.amount) + assertEquals(1693876497L, response.result?.created_at) + assertEquals(1694876497L, response.result?.expires_at) + } + + // --- LookupInvoice Success --- + + @Test + fun testLookupInvoiceSuccessDeserialization() { + val json = """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash123","amount":1000,"settled_at":1694876497}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result) + assertEquals("incoming", response.result?.type) + assertEquals("settled", response.result?.state) + assertEquals("hash123", response.result?.payment_hash) + assertEquals(1000L, response.result?.amount) + assertEquals(1694876497L, response.result?.settled_at) + } + + // --- ListTransactions Success --- + + @Test + fun testListTransactionsSuccessDeserialization() { + val json = + """{"result_type":"list_transactions","result":{"transactions":[{"type":"incoming","invoice":"lnbc1...","amount":100,"created_at":1000},{"type":"outgoing","invoice":"lnbc2...","amount":200,"created_at":2000}]}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result?.transactions) + assertEquals(2, response.result?.transactions?.size) + assertEquals("incoming", response.result?.transactions?.get(0)?.type) + assertEquals(100L, response.result?.transactions?.get(0)?.amount) + assertEquals("outgoing", response.result?.transactions?.get(1)?.type) + assertEquals(200L, response.result?.transactions?.get(1)?.amount) + } + + @Test + fun testListTransactionsEmptyResult() { + val json = """{"result_type":"list_transactions","result":{"transactions":[]}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result?.transactions) + assertEquals(0, response.result?.transactions?.size) + } + + // --- GetBalance Success --- + + @Test + fun testGetBalanceSuccessDeserialization() { + val json = """{"result_type":"get_balance","result":{"balance":21000}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(21000L, response.result?.balance) + } + + @Test + fun testGetBalanceZero() { + val json = """{"result_type":"get_balance","result":{"balance":0}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(0L, response.result?.balance) + } + + // --- GetInfo Success --- + + @Test + fun testGetInfoSuccessDeserialization() { + val json = + """{"result_type":"get_info","result":{"alias":"MyNode","color":"#ff9900","pubkey":"abc123","network":"mainnet","block_height":800000,"block_hash":"hash","methods":["pay_invoice","get_balance"],"notifications":["payment_received"]}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result) + assertEquals("MyNode", response.result?.alias) + assertEquals("#ff9900", response.result?.color) + assertEquals("abc123", response.result?.pubkey) + assertEquals("mainnet", response.result?.network) + assertEquals(800000L, response.result?.block_height) + assertEquals("hash", response.result?.block_hash) + assertEquals(listOf("pay_invoice", "get_balance"), response.result?.methods) + assertEquals(listOf("payment_received"), response.result?.notifications) + } + + // --- MakeHoldInvoice Success --- + + @Test + fun testMakeHoldInvoiceSuccessDeserialization() { + val json = """{"result_type":"make_hold_invoice","result":{"type":"incoming","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"expires_at":2000}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result) + assertEquals("lnbc...", response.result?.invoice) + assertEquals("hash", response.result?.payment_hash) + } + + // --- CancelHoldInvoice Success --- + + @Test + fun testCancelHoldInvoiceSuccessDeserialization() { + val json = """{"result_type":"cancel_hold_invoice","result":{}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + } + + // --- SettleHoldInvoice Success --- + + @Test + fun testSettleHoldInvoiceSuccessDeserialization() { + val json = """{"result_type":"settle_hold_invoice","result":{}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + } + + // --- Generic Error Response --- + + @Test + fun testGenericErrorForGetBalance() { + val json = """{"result_type":"get_balance","error":{"code":"UNAUTHORIZED","message":"No wallet connected"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("get_balance", response.resultType) + assertEquals(NwcErrorCode.UNAUTHORIZED, response.error?.code) + assertEquals("No wallet connected", response.error?.message) + } + + @Test + fun testGenericErrorForGetInfo() { + val json = """{"result_type":"get_info","error":{"code":"NOT_IMPLEMENTED","message":"Not supported"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("get_info", response.resultType) + assertEquals(NwcErrorCode.NOT_IMPLEMENTED, response.error?.code) + } + + @Test + fun testGenericErrorForMakeInvoice() { + val json = """{"result_type":"make_invoice","error":{"code":"QUOTA_EXCEEDED","message":"Spending limit reached"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.QUOTA_EXCEEDED, response.error?.code) + } + + @Test + fun testGenericErrorRateLimited() { + val json = """{"result_type":"pay_keysend","error":{"code":"RATE_LIMITED","message":"Too many requests"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.RATE_LIMITED, response.error?.code) + } + + @Test + fun testGenericErrorUnsupportedEncryption() { + val json = """{"result_type":"pay_invoice","error":{"code":"UNSUPPORTED_ENCRYPTION","message":"Use nip44"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + // pay_invoice errors go to PayInvoiceErrorResponse for backward compat + assertIs(response) + } + + // --- Null/missing result --- + + @Test + fun testResponseWithNoResultOrError() { + val json = """{"result_type":"pay_invoice"}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + // Should still deserialize since result_type is present + assertIs(response) + assertNull(response.result) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/TagsTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/TagsTest.kt new file mode 100644 index 0000000000..8010589356 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/TagsTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip47WalletConnect.tags.EncryptionTag +import com.vitorpamplona.quartz.nip47WalletConnect.tags.NotificationsTag +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class TagsTest { + // --- EncryptionTag --- + + @Test + fun testEncryptionTagParse() { + val tag = arrayOf("encryption", "nip44_v2", "nip04") + val result = EncryptionTag.parse(tag) + assertNotNull(result) + assertEquals(listOf("nip44_v2", "nip04"), result) + } + + @Test + fun testEncryptionTagParseSingleScheme() { + val tag = arrayOf("encryption", "nip44_v2") + val result = EncryptionTag.parse(tag) + assertNotNull(result) + assertEquals(listOf("nip44_v2"), result) + } + + @Test + fun testEncryptionTagParseWrongTagName() { + val tag = arrayOf("other", "nip44_v2") + val result = EncryptionTag.parse(tag) + assertNull(result) + } + + @Test + fun testEncryptionTagParseTooShort() { + val tag = arrayOf("encryption") + val result = EncryptionTag.parse(tag) + assertNull(result) + } + + @Test + fun testEncryptionTagParseEmptyValue() { + val tag = arrayOf("encryption", "") + val result = EncryptionTag.parse(tag) + assertNull(result) + } + + @Test + fun testEncryptionTagAssemble() { + val tag = EncryptionTag.assemble(listOf("nip44_v2", "nip04")) + assertEquals("encryption", tag[0]) + assertEquals("nip44_v2", tag[1]) + assertEquals("nip04", tag[2]) + assertEquals(3, tag.size) + } + + @Test + fun testEncryptionTagIsTag() { + assertTrue(EncryptionTag.isTag(arrayOf("encryption", "nip44_v2"))) + assertFalse(EncryptionTag.isTag(arrayOf("other", "nip44_v2"))) + assertFalse(EncryptionTag.isTag(arrayOf("encryption"))) + assertFalse(EncryptionTag.isTag(arrayOf("encryption", ""))) + } + + // --- NotificationsTag --- + + @Test + fun testNotificationsTagParse() { + val tag = arrayOf("notifications", "payment_received", "payment_sent") + val result = NotificationsTag.parse(tag) + assertNotNull(result) + assertEquals(listOf("payment_received", "payment_sent"), result) + } + + @Test + fun testNotificationsTagParseSingleType() { + val tag = arrayOf("notifications", "payment_received") + val result = NotificationsTag.parse(tag) + assertNotNull(result) + assertEquals(listOf("payment_received"), result) + } + + @Test + fun testNotificationsTagParseWrongTagName() { + val tag = arrayOf("other", "payment_received") + val result = NotificationsTag.parse(tag) + assertNull(result) + } + + @Test + fun testNotificationsTagParseTooShort() { + val tag = arrayOf("notifications") + val result = NotificationsTag.parse(tag) + assertNull(result) + } + + @Test + fun testNotificationsTagAssemble() { + val tag = NotificationsTag.assemble(listOf("payment_received", "payment_sent", "hold_invoice_accepted")) + assertEquals("notifications", tag[0]) + assertEquals("payment_received", tag[1]) + assertEquals("payment_sent", tag[2]) + assertEquals("hold_invoice_accepted", tag[3]) + assertEquals(4, tag.size) + } + + @Test + fun testNotificationsTagIsTag() { + assertTrue(NotificationsTag.isTag(arrayOf("notifications", "payment_received"))) + assertFalse(NotificationsTag.isTag(arrayOf("other", "payment_received"))) + assertFalse(NotificationsTag.isTag(arrayOf("notifications"))) + assertFalse(NotificationsTag.isTag(arrayOf("notifications", ""))) + } +} From c0bd88278ca387245c5f502706b24ce8d4e490ea Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 13 Mar 2026 12:20:55 +0000 Subject: [PATCH 044/172] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-zh-rCN/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index a0630ce1df..86345bfd51 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -928,6 +928,7 @@ 请确保签名应用程序已授权此交易 找不到支付闪电发票的钱包(错误:%1$s)。请安装闪电钱包来使用打闪 找不到支付闪电发票的钱包。请安装闪电钱包来使用打闪 + 找不到Blossom应用。请安装本地Blossom应用查看此文件 隐藏单词 隐藏新单词或句子 个人头像 @@ -1271,6 +1272,12 @@ 搜索话题标签:#%1$s 不要翻译 此处显示的语言不会被翻译,请选择一种目标语言重新翻译并去除这个提示。 + 选择要将内容翻译为什么语言。 + 语言显示首选项 + 对于每个翻译的语言对,选择先显示哪个语言。 + %1$s → %2$s + 尚无语言显示首选项。这些在翻译发生时自动创建,或者您可以手动添加。 + 删除首选项 暂停 播放 打开下拉菜单 From 9ca79acbe4dbf792a37f14b936e4622847ba0bef Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 08:21:55 -0400 Subject: [PATCH 045/172] lint --- .../amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index 34ec325474..755b98ccfe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -122,7 +122,8 @@ fun UserSettingsScreen( private fun getAllLanguagesSorted(): List { val seen = mutableSetOf() - return JavaLocale.getAvailableLocales() + return JavaLocale + .getAvailableLocales() .filter { it.language.isNotBlank() && it.country.isBlank() && seen.add(it.language) } .sortedBy { it.displayName.lowercase() } } From c778c9f109ad370ac58935496c1cf89855a80c4f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 08:40:28 -0400 Subject: [PATCH 046/172] Adds a hook to get claude web to compile and check android code --- .claude/hooks/session-start.sh | 180 +++++++++++++++++++++++++++++++++ .claude/settings.json | 10 ++ 2 files changed, 190 insertions(+) create mode 100644 .claude/hooks/session-start.sh diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh new file mode 100644 index 0000000000..35cddfb3b4 --- /dev/null +++ b/.claude/hooks/session-start.sh @@ -0,0 +1,180 @@ +#!/bin/bash +# Session start hook: Configure proxy auth, SSL trust, and Android SDK for Claude Code on the web +set -euo pipefail + +# Only run in remote (web) environments +if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then + exit 0 +fi + +# --- Proxy credentials: configure Maven/Gradle if authenticated proxy is set --- +proxy="${https_proxy:-${HTTPS_PROXY:-}}" +if [ -n "$proxy" ] && echo "$proxy" | grep -q '@'; then + rest="${proxy#*://}" + userpass="${rest%@*}" + hostport="${rest##*@}" + user="${userpass%%:*}" + pass="${userpass#*:}" + host="${hostport%%:*}" + port="${hostport##*:}" + port="${port%/}" + + mkdir -p ~/.m2 + cat > ~/.m2/settings.xml << EOF + + + + ccwtruehttps + $host$port + $user + + + + +EOF + + # Force wagon transport for Maven 3.9+ proxy auth compatibility + cat > ~/.mavenrc << 'MAVENRC' +MAVEN_OPTS="$MAVEN_OPTS -Dmaven.resolver.transport=wagon" +MAVENRC + + mkdir -p ~/.gradle + cat > ~/.gradle/gradle.properties << EOF +systemProp.https.proxyHost=$host +systemProp.https.proxyPort=$port +systemProp.https.proxyUser=$user +systemProp.https.proxyPassword=$pass +systemProp.http.proxyHost=$host +systemProp.http.proxyPort=$port +systemProp.http.proxyUser=$user +systemProp.http.proxyPassword=$pass +# Override nonProxyHosts: route all external traffic (incl. *.google.com) through proxy +systemProp.http.nonProxyHosts=localhost|127.0.0.1 +systemProp.https.nonProxyHosts=localhost|127.0.0.1 +# Use Ubuntu's Java trust store (includes Anthropic TLS inspection CA) for all Gradle JVMs. +# This is needed because Gradle may download a custom JDK (e.g. JetBrains) whose bundled +# trust store doesn't include the Anthropic CA, causing TLS inspection failures. +systemProp.javax.net.ssl.trustStore=/etc/ssl/certs/java/cacerts +systemProp.javax.net.ssl.trustStoreType=JKS +systemProp.javax.net.ssl.trustStorePassword=changeit +systemProp.jdk.http.auth.tunneling.disabledSchemes= +systemProp.jdk.http.auth.proxying.disabledSchemes= +EOF + + echo "Configured Maven/Gradle proxy from HTTPS_PROXY" >&2 +fi + +# --- SSL trust: import Anthropic TLS inspection CA into JVM trust stores --- +ANTHROPIC_CA_PEM=$(python3 -c " +import re, ssl, sys +try: + with open('/etc/ssl/certs/ca-certificates.crt') as f: + certs = re.findall(r'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', f.read(), re.DOTALL) + for cert in certs: + der = ssl.PEM_cert_to_DER_cert(cert) + if b'Anthropic' in der and b'sandbox-egress-production' in der: + print(cert) + break +except Exception as e: + sys.stderr.write(f'CA extraction failed: {e}\n') +" 2>/dev/null) + +if [ -n "$ANTHROPIC_CA_PEM" ]; then + TMPCA=$(mktemp /tmp/anthropic-ca.XXXXXX.pem) + echo "$ANTHROPIC_CA_PEM" > "$TMPCA" + for cacerts in \ + /usr/lib/jvm/java-21-openjdk-amd64/lib/security/cacerts \ + /root/.gradle/jdks/*/lib/security/cacerts; do + [ -f "$cacerts" ] || continue + keytool -list -keystore "$cacerts" -storepass changeit \ + -alias anthropic-egress-production-ca >/dev/null 2>&1 && continue + keytool -import \ + -alias anthropic-egress-production-ca \ + -file "$TMPCA" \ + -keystore "$cacerts" \ + -storepass changeit \ + -noprompt >/dev/null 2>&1 && \ + echo "Imported Anthropic CA into $cacerts" >&2 + done + rm -f "$TMPCA" +fi + +ANDROID_SDK_DIR="/root/android-sdk" +SDK_REPO_BASE="https://dl.google.com/android/repository" + +# Install Android SDK packages by downloading directly with curl +# (sdkmanager cannot reach the SDK repository through the proxy) +install_sdk_package() { + local zip_url="$1" + local dest_dir="$2" + local inner_dir="$3" # top-level dir inside the zip + + if [ -d "$dest_dir" ]; then + return 0 + fi + + echo "Downloading $zip_url..." + local TMP_ZIP + TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) + curl -fsSL "$zip_url" -o "$TMP_ZIP" + + local TMP_DIR + TMP_DIR=$(mktemp -d) + unzip -q "$TMP_ZIP" -d "$TMP_DIR" + rm -f "$TMP_ZIP" + + mkdir -p "$(dirname "$dest_dir")" + mv "$TMP_DIR/$inner_dir" "$dest_dir" + rm -rf "$TMP_DIR" + echo "Installed to $dest_dir" +} + +# Install Android platform 36 +install_sdk_package \ + "$SDK_REPO_BASE/platform-36_r02.zip" \ + "$ANDROID_SDK_DIR/platforms/android-36" \ + "android-36" + +# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) +install_sdk_package \ + "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ + "$ANDROID_SDK_DIR/build-tools/36.0.0" \ + "android-16" + +# Install platform-tools +install_sdk_package \ + "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ + "$ANDROID_SDK_DIR/platform-tools" \ + "platform-tools" + +# Accept SDK licenses (create license files manually) +echo "Writing SDK license files..." +mkdir -p "$ANDROID_SDK_DIR/licenses" +# android-sdk-license +echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" +echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" +# android-sdk-preview-license +echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" +echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" +# intel-android-extra-license +echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" + +# Create local.properties if missing +REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/Amber}")" +LOCAL_PROPS="$REPO_ROOT/local.properties" +if [ ! -f "$LOCAL_PROPS" ]; then + echo "sdk.dir=$ANDROID_SDK_DIR" > "$LOCAL_PROPS" + echo "Created local.properties with sdk.dir=$ANDROID_SDK_DIR" +fi + +# Export ANDROID_HOME for the session +if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + echo "export ANDROID_HOME=$ANDROID_SDK_DIR" >> "$CLAUDE_ENV_FILE" + echo "export ANDROID_SDK_ROOT=$ANDROID_SDK_DIR" >> "$CLAUDE_ENV_FILE" + echo "export PATH=\$PATH:$ANDROID_SDK_DIR/platform-tools" >> "$CLAUDE_ENV_FILE" +fi + +cd "$CLAUDE_PROJECT_DIR" +./gradlew --version > /dev/null 2>&1 + +echo "Android SDK setup complete." \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json index fb09a8e87d..af65d2dbca 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,5 +1,15 @@ { "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh" + } + ] + } + ] "Stop": [ { "matcher": "", From 3e0b8a5f8fcc299a10707fd4b152f9c1f824490c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 08:40:51 -0400 Subject: [PATCH 047/172] Fixes gradle build on MacOS and adds a Clibsodium.def file --- quartz/build.gradle.kts | 34 +++++++++---------- .../src/nativeInterop/cinterop/Clibsodium.def | 3 ++ 2 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 quartz/src/nativeInterop/cinterop/Clibsodium.def diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 015bc4e39d..488f904f11 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -63,23 +63,18 @@ kotlin { val xcfName = "quartz-kmpKit" val libsodiumPath = project.file("src/nativeInterop/libsodium") val libsodiumHeaderFilesPath = project.file("$libsodiumPath/include/sodium") + val libsodiumDefFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") // This generates the Libsodium definition file, necessary for creating native bindings(a Kotlin API) for libsodium(for iOS). -// val headersTask = tasks.register("ExtractLibsodiumHeaders") { -// val unpackDirectory = "$libsodiumPath/include" -// unzipTo( -// project.file(unpackDirectory), -// project.file("$unpackDirectory/sodium.zip") -// ) -// } - - val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { - val definitionFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") - if (!definitionFile.exists()){ - definitionFile.createNewFile() - definitionFile.writeText("package = Clibsodium") - definitionFile.appendText("\n" + "staticLibraries = libsodium.a libsodium-simulator.a") - definitionFile.appendText("\n" + "libraryPaths = ${libsodiumPath}/ios/lib ${libsodiumPath}/ios-simulators/lib") + val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { + outputs.file(libsodiumDefFile) + doLast { + if (!libsodiumDefFile.exists()) { + libsodiumDefFile.parentFile.mkdirs() + libsodiumDefFile.writeText("package = Clibsodium\n") + libsodiumDefFile.appendText("staticLibraries = libsodium.a libsodium-simulator.a\n") + libsodiumDefFile.appendText("libraryPaths = ${libsodiumPath.absolutePath}/ios/lib ${libsodiumPath.absolutePath}/ios-simulators/lib\n") + } } } @@ -90,10 +85,9 @@ kotlin { ).forEach { target -> target.compilations.getByName("main") { -// headersTask.get() val Clibsodium by cinterops.creating { - assert(project.file("Clibsodium.def").exists()) - definitionFile.set(project.file("Clibsodium.def")) + definitionFile = libsodiumDefFile + packageName = "Clibsodium" headers( "$libsodiumHeaderFilesPath/crypto_aead_xchacha20poly1305.h", @@ -101,6 +95,10 @@ kotlin { "$libsodiumHeaderFilesPath/crypto_stream_chacha20.h" ) } + + tasks.named(cinterops.getByName("Clibsodium").interopProcessingTaskName).configure { + dependsOn(libsodiumDefFileGeneration) + } } target.swiftPackageConfig(cinteropName = "swiftbridge") { diff --git a/quartz/src/nativeInterop/cinterop/Clibsodium.def b/quartz/src/nativeInterop/cinterop/Clibsodium.def new file mode 100644 index 0000000000..a60f3fda69 --- /dev/null +++ b/quartz/src/nativeInterop/cinterop/Clibsodium.def @@ -0,0 +1,3 @@ +package = Clibsodium +staticLibraries = libsodium.a libsodium-simulator.a +libraryPaths = src/nativeInterop/libsodium/ios/lib src/nativeInterop/libsodium/ios-simulators/lib From 4bf7f982bac7b990c5f856cea5bc8119b2a3b4c9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 09:24:12 -0400 Subject: [PATCH 048/172] Makes the new translation settings work --- .../vitorpamplona/amethyst/model/Account.kt | 10 ++ .../amethyst/model/AccountSettings.kt | 14 +- .../amethyst/model/AccountSyncedSettings.kt | 71 +++++----- .../ui/screen/loggedIn/AccountViewModel.kt | 10 +- .../loggedIn/settings/AppSettingsScreen.kt | 30 +++++ .../loggedIn/settings/UserSettingsScreen.kt | 121 +++++++++++------- 6 files changed, 172 insertions(+), 84 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 055cb6ae28..84e3cc0f7a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -497,6 +497,16 @@ class Account( sendNewAppSpecificData() } + suspend fun addDontTranslateFrom(languageCode: String) { + settings.addDontTranslateFrom(languageCode) + sendNewAppSpecificData() + } + + suspend fun removeDontTranslateFrom(languageCode: String) { + settings.removeDontTranslateFrom(languageCode) + sendNewAppSpecificData() + } + suspend fun updateTranslateTo(languageCode: Locale) { if (settings.updateTranslateTo(languageCode)) { sendNewAppSpecificData() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 57538f5a8e..5e6ad853d2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -322,7 +322,19 @@ class AccountSettings( saveAccountSettings() } - fun translateToContains(languageCode: Locale) = syncedSettings.languages.translateTo.contains(languageCode.language) + fun addDontTranslateFrom(languageCode: String) { + syncedSettings.languages.addDontTranslateFrom(languageCode) + saveAccountSettings() + } + + fun removeDontTranslateFrom(languageCode: String) { + syncedSettings.languages.removeDontTranslateFrom(languageCode) + saveAccountSettings() + } + + fun translateToContains(languageCode: Locale) = + syncedSettings.languages.translateTo.value + .contains(languageCode.language) fun updateTranslateTo(languageCode: Locale): Boolean { if (syncedSettings.languages.updateTranslateTo(languageCode)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index eb5006401b..3789e1c983 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -45,9 +45,9 @@ class AccountSyncedSettings( ) val languages = AccountLanguagePreferences( - internalSettings.languages.dontTranslateFrom, - internalSettings.languages.languagePreferences, - internalSettings.languages.translateTo, + MutableStateFlow(internalSettings.languages.dontTranslateFrom), + MutableStateFlow(internalSettings.languages.languagePreferences), + MutableStateFlow(internalSettings.languages.translateTo), ) val security = AccountSecurityPreferences( @@ -66,9 +66,9 @@ class AccountSyncedSettings( ), languages = AccountLanguagePreferencesInternal( - languages.dontTranslateFrom, - languages.languagePreferences, - languages.translateTo, + languages.dontTranslateFrom.value, + languages.languagePreferences.value, + languages.translateTo.value, ), security = AccountSecurityPreferencesInternal( @@ -98,16 +98,16 @@ class AccountSyncedSettings( zaps.defaultZapType.tryEmit(syncedSettingsInternal.zaps.defaultZapType) } - if (languages.dontTranslateFrom != syncedSettingsInternal.languages.dontTranslateFrom) { - languages.dontTranslateFrom = syncedSettingsInternal.languages.dontTranslateFrom + if (languages.dontTranslateFrom.value != syncedSettingsInternal.languages.dontTranslateFrom) { + languages.dontTranslateFrom.value = syncedSettingsInternal.languages.dontTranslateFrom } - if (languages.languagePreferences != syncedSettingsInternal.languages.languagePreferences) { - languages.languagePreferences = syncedSettingsInternal.languages.languagePreferences + if (languages.languagePreferences.value != syncedSettingsInternal.languages.languagePreferences) { + languages.languagePreferences.value = syncedSettingsInternal.languages.languagePreferences } - if (languages.translateTo != syncedSettingsInternal.languages.translateTo) { - languages.translateTo = syncedSettingsInternal.languages.translateTo + if (languages.translateTo.value != syncedSettingsInternal.languages.translateTo) { + languages.translateTo.value = syncedSettingsInternal.languages.translateTo } if (security.showSensitiveContent.value != syncedSettingsInternal.security.showSensitiveContent) { @@ -123,7 +123,7 @@ class AccountSyncedSettings( } } - fun dontTranslateFromFilteredBySpokenLanguages(): Set = languages.dontTranslateFrom - getLanguagesSpokenByUser() + fun dontTranslateFromFilteredBySpokenLanguages(): Set = languages.dontTranslateFrom.value - getLanguagesSpokenByUser() } @Stable @@ -140,27 +140,36 @@ class AccountZapPreferences( @Stable class AccountLanguagePreferences( - var dontTranslateFrom: Set, - var languagePreferences: Map, - var translateTo: String, + var dontTranslateFrom: MutableStateFlow>, + var languagePreferences: MutableStateFlow>, + var translateTo: MutableStateFlow, ) { // --- // language services // --- fun toggleDontTranslateFrom(languageCode: String) { - dontTranslateFrom = - if (!dontTranslateFrom.contains(languageCode)) { - dontTranslateFrom.plus(languageCode) + dontTranslateFrom.update { + if (it.contains(languageCode)) { + it - languageCode } else { - dontTranslateFrom.minus(languageCode) + it + languageCode } + } } - fun translateToContains(languageCode: Locale) = translateTo.contains(languageCode.language) + fun addDontTranslateFrom(languageCode: String) { + dontTranslateFrom.update { it + languageCode } + } + + fun removeDontTranslateFrom(languageCode: String) { + dontTranslateFrom.update { it - languageCode } + } + + fun translateToContains(languageCode: Locale) = translateTo.value.contains(languageCode.language) fun updateTranslateTo(languageCode: Locale): Boolean { - if (translateTo != languageCode.language) { - translateTo = languageCode.language + if (translateTo.value != languageCode.language) { + translateTo.tryEmit(languageCode.language) return true } return false @@ -172,13 +181,15 @@ class AccountLanguagePreferences( preference: String, ) { val key = "$source,$target" - if (key !in languagePreferences) { - languagePreferences = languagePreferences + Pair(key, preference) - } else { - if (languagePreferences.get(key) == preference) { - languagePreferences = languagePreferences.minus(key) + languagePreferences.update { + if (key !in it) { + it + Pair(key, preference) } else { - languagePreferences = languagePreferences + Pair(key, preference) + if (it.get(key) == preference) { + it.minus(key) + } else { + it + Pair(key, preference) + } } } } @@ -186,7 +197,7 @@ class AccountLanguagePreferences( fun preferenceBetween( source: String, target: String, - ): String? = languagePreferences["$source,$target"] + ): String? = languagePreferences.value["$source,$target"] } @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 3ae5a63e69..09a901b7d8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -953,11 +953,9 @@ class AccountViewModel( fun markDonatedInThisVersion() = account.markDonatedInThisVersion() - fun dontTranslateFrom() = account.settings.syncedSettings.languages.dontTranslateFrom + fun dontTranslateFrom() = account.settings.syncedSettings.languages.dontTranslateFrom.value - fun dontTranslateFromFilteredBySpokenLanguages() = account.settings.syncedSettings.dontTranslateFromFilteredBySpokenLanguages() - - fun translateTo() = account.settings.syncedSettings.languages.translateTo + fun translateTo() = account.settings.syncedSettings.languages.translateTo.value fun defaultZapType() = account.settings.syncedSettings.zaps.defaultZapType.value @@ -1007,6 +1005,10 @@ class AccountViewModel( fun toggleDontTranslateFrom(languageCode: String) = launchSigner { account.toggleDontTranslateFrom(languageCode) } + fun addDontTranslateFrom(languageCode: String) = launchSigner { account.addDontTranslateFrom(languageCode) } + + fun removeDontTranslateFrom(languageCode: String) = launchSigner { account.removeDontTranslateFrom(languageCode) } + fun updateTranslateTo(languageCode: Locale) = launchSigner { account.updateTranslateTo(languageCode) } fun prefer( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 2ab3d05392..39bb49260c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -422,3 +422,33 @@ fun SettingsRow( } } } + +@Composable +fun SettingsRow( + name: Int, + description: Int, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + ) { + Column( + modifier = Modifier.weight(2.0f), + verticalArrangement = Arrangement.spacedBy(3.dp), + ) { + Text( + text = stringRes(name), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index 755b98ccfe..ef2f01e2ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings +import android.R.attr.targetName import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -40,7 +41,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Search @@ -67,6 +67,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -79,6 +80,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20dp import com.vitorpamplona.amethyst.ui.theme.SpacedBy10dp import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow +import okio.`-DeprecatedOkio`.source import java.util.Locale as JavaLocale @Preview(device = "spec:width=2160px,height=2340px,dpi=440") @@ -193,7 +195,8 @@ private fun SearchableLanguageList( @Composable fun TranslateToSetting(accountViewModel: AccountViewModel) { - val currentTranslateTo = accountViewModel.translateTo() + val currentTranslateTo by accountViewModel.account.settings.syncedSettings.languages.translateTo + .collectAsStateWithLifecycle() val allLanguages = remember { getAllLanguagesSorted() } var showPicker by remember { mutableStateOf(false) } @@ -235,7 +238,8 @@ fun TranslateToSetting(accountViewModel: AccountViewModel) { @OptIn(ExperimentalLayoutApi::class) @Composable fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { - val selectedLanguages = accountViewModel.dontTranslateFrom().toSet() + val selectedLanguages by accountViewModel.account.settings.syncedSettings.languages.dontTranslateFrom + .collectAsStateWithLifecycle() var showAddPicker by remember { mutableStateOf(false) } val allLanguages = remember { getAllLanguagesSorted() } @@ -248,7 +252,7 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { SettingsRow( name = R.string.dont_translate_from, description = R.string.dont_translate_from_description, - ) {} + ) Spacer(modifier = Modifier.height(8.dp)) @@ -260,7 +264,7 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { selectedLanguages.forEach { languageCode -> InputChip( selected = true, - onClick = { accountViewModel.toggleDontTranslateFrom(languageCode) }, + onClick = { accountViewModel.removeDontTranslateFrom(languageCode) }, label = { Text(JavaLocale(languageCode).displayName) }, trailingIcon = { Icon( @@ -292,7 +296,7 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { SearchableLanguageList( languages = availableToAdd, onSelect = { locale -> - accountViewModel.toggleDontTranslateFrom(locale.language) + accountViewModel.addDontTranslateFrom(locale.language) showAddPicker = false }, ) @@ -302,14 +306,16 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { @Composable fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) { - val languagePreferences = accountViewModel.account.settings.syncedSettings.languages.languagePreferences + val languagePreferences by + accountViewModel.account.settings.syncedSettings.languages.languagePreferences + .collectAsStateWithLifecycle() var showAddPair by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { SettingsRow( name = R.string.language_preferences, description = R.string.language_preferences_description, - ) {} + ) if (languagePreferences.isEmpty() && !showAddPair) { Text( @@ -332,20 +338,20 @@ fun LanguagePreferencesSetting(accountViewModel: AccountViewModel) { } } - TextButton( - onClick = { showAddPair = !showAddPair }, - modifier = Modifier.align(Alignment.CenterHorizontally), - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(modifier = Modifier.width(4.dp)) - Text(stringRes(R.string.add_language_pair)) - } - - if (showAddPair) { + if (!showAddPair) { + TextButton( + onClick = { showAddPair = !showAddPair }, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(4.dp)) + Text(stringRes(R.string.add_language_pair)) + } + } else { AddLanguagePairCard( accountViewModel = accountViewModel, onDismiss = { showAddPair = false }, @@ -448,7 +454,8 @@ private fun AddLanguagePairCard( val allLanguages = remember { getAllLanguagesSorted() } var selectedSource by remember { mutableStateOf(null) } var selectedTarget by remember { mutableStateOf(null) } - var pickingSource by remember { mutableStateOf(true) } + var selectedPreference by remember { mutableStateOf(null) } + var pickingSource by remember { mutableStateOf(false) } var pickingTarget by remember { mutableStateOf(false) } OutlinedCard(modifier = Modifier.fillMaxWidth()) { @@ -557,41 +564,57 @@ private fun AddLanguagePairCard( ) } + val selectedSource = selectedSource + val selectedTarget = selectedTarget if (selectedSource != null && selectedTarget != null) { Spacer(modifier = Modifier.height(12.dp)) Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, + modifier = + Modifier + .fillMaxWidth() + .clickable { + accountViewModel.prefer(selectedSource.language, selectedTarget.language, selectedSource.language) + onDismiss() + }.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { - TextButton(onClick = onDismiss) { - Text(stringRes(R.string.go_back)) - } - - Spacer(modifier = Modifier.width(8.dp)) - - TextButton( + RadioButton( + selected = selectedPreference == selectedSource, onClick = { - val src = selectedSource!!.language - val tgt = selectedTarget!!.language - accountViewModel.prefer(src, tgt, tgt) + accountViewModel.prefer(selectedSource.language, selectedTarget.language, selectedSource.language) onDismiss() }, - ) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - stringRes( - R.string.show_first, - selectedTarget!!.displayName, - ), - ) - } + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringRes(R.string.show_first, selectedSource.displayName), + style = MaterialTheme.typography.bodyMedium, + ) + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable { + accountViewModel.prefer(selectedSource.language, selectedTarget.language, selectedTarget.language) + onDismiss() + }.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + selected = selectedPreference == selectedTarget, + onClick = { + accountViewModel.prefer(selectedSource.language, selectedTarget.language, selectedTarget.language) + onDismiss() + }, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringRes(R.string.show_first, selectedTarget.displayName), + style = MaterialTheme.typography.bodyMedium, + ) } } } From 178e47ebf5a3f202f2a24ec27ef771dbe752ee35 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 13 Mar 2026 13:28:43 +0000 Subject: [PATCH 049/172] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-zh-rCN/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 86345bfd51..6d7eafd38e 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -928,6 +928,7 @@ 请确保签名应用程序已授权此交易 找不到支付闪电发票的钱包(错误:%1$s)。请安装闪电钱包来使用打闪 找不到支付闪电发票的钱包。请安装闪电钱包来使用打闪 + 无法打开 Blossom 链接 找不到Blossom应用。请安装本地Blossom应用查看此文件 隐藏单词 隐藏新单词或句子 @@ -1272,10 +1273,17 @@ 搜索话题标签:#%1$s 不要翻译 此处显示的语言不会被翻译,请选择一种目标语言重新翻译并去除这个提示。 + 翻译为 选择要将内容翻译为什么语言。 语言显示首选项 对于每个翻译的语言对,选择先显示哪个语言。 %1$s → %2$s + 搜索语言 + 添加语言 + 添加语言对 + 源语言 + 目标语言 + 先显示 %1$s 尚无语言显示首选项。这些在翻译发生时自动创建,或者您可以手动添加。 删除首选项 暂停 From f4d6e3cb3e20e482ff14615f23d58cb581ecd404 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 13:22:44 -0400 Subject: [PATCH 050/172] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/main/java/com/vitorpamplona/amethyst/AppModules.kt | 6 ++++++ .../ui/screen/loggedIn/settings/OtsSettingsSection.kt | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index 25db5b3f4c..46e068a16c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -117,6 +117,12 @@ class AppModules( OtsSharedPreferences(appContext, applicationIOScope) } + init { + applicationIOScope.launch { + // Eagerly initialize OtsSharedPreferences off the main thread + otsPrefs + } + } // App services that should be run as soon as there are subscribers to their flows val locationManager = LocationState(appContext, applicationIOScope) val connManager = ConnectivityManager(appContext, applicationIOScope) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsSection.kt index 5f2e861aba..380c774427 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/OtsSettingsSection.kt @@ -234,7 +234,7 @@ private fun CustomExplorerInput( onSave: (String?) -> Unit, ) { var input by rememberSaveable(currentUrl) { mutableStateOf(currentUrl ?: "") } - var validationError by remember { mutableStateOf(null) } + var validationError by remember(currentUrl) { mutableStateOf(null) } val kb = LocalSoftwareKeyboardController.current fun trySave() { From 8bc14b52b2ef8f9f6f784b7e76ab784f7638f318 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 13:25:23 -0400 Subject: [PATCH 051/172] Spotless update --- .../src/main/java/com/vitorpamplona/amethyst/AppModules.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index c600d90b50..e9e7c6f6fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -31,9 +31,9 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier import com.vitorpamplona.amethyst.model.nip03Timestamp.TorAwareOkHttpOtsResolverBuilder -import com.vitorpamplona.amethyst.model.preferences.OtsSharedPreferences import com.vitorpamplona.amethyst.model.nip11RelayInfo.Nip11CachedRetriever import com.vitorpamplona.amethyst.model.preferences.NamecoinSharedPreferences +import com.vitorpamplona.amethyst.model.preferences.OtsSharedPreferences import com.vitorpamplona.amethyst.model.preferences.TorSharedPreferences import com.vitorpamplona.amethyst.model.preferences.UiSharedPreferences import com.vitorpamplona.amethyst.model.privacyOptions.RoleBasedHttpClientBuilder @@ -135,6 +135,7 @@ class AppModules( otsPrefs } } + // App services that should be run as soon as there are subscribers to their flows val locationManager = LocationState(appContext, applicationIOScope) val connManager = ConnectivityManager(appContext, applicationIOScope) From e83ec94461c6bc8686040ed26b66cbab6143ba77 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 17:28:39 +0000 Subject: [PATCH 052/172] refactor: extract relay export logic into RelayExporter class Moves the text formatting and share intent logic from AllRelayListScreen into a dedicated RelayExporter class in the common package. https://claude.ai/code/session_013PhuahpBFh6djVHzSsmNKM --- .../loggedIn/relays/AllRelayListScreen.kt | 165 +----------------- .../loggedIn/relays/common/RelayExporter.kt | 125 +++++++++++++ 2 files changed, 127 insertions(+), 163 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index ef41607dee..7a0902a2b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays -import android.content.Context -import android.content.Intent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.consumeWindowInsets @@ -84,7 +82,7 @@ import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter import com.vitorpamplona.amethyst.ui.theme.grayText @Composable @@ -191,8 +189,7 @@ fun MappedAllRelayListView( titleRes = R.string.relay_settings, additionalActions = { IconButton(onClick = { - exportRelaySettings( - context = context, + RelayExporter(context).export( homeRelays = homeFeedState, notifRelays = notifFeedState, dmRelays = dmFeedState, @@ -482,161 +479,3 @@ fun SettingsCategoryWithButton( } } -private fun formatRelaySection( - title: String, - description: String, - relays: List, - builder: StringBuilder, -) { - if (relays.isEmpty()) return - builder.appendLine("## $title") - builder.appendLine("# $description") - builder.appendLine() - relays.forEach { relay -> - builder.appendLine(relay.relay.url) - } - builder.appendLine() -} - -fun buildRelaySettingsText( - context: Context, - homeRelays: List, - notifRelays: List, - dmRelays: List, - privateOutboxRelays: List, - proxyRelays: List, - broadcastRelays: List, - indexerRelays: List, - searchRelays: List, - localRelays: List, - trustedRelays: List, - favoriteRelays: List, - blockedRelays: List, -): String { - val builder = StringBuilder() - builder.appendLine("# ${context.getString(R.string.relay_settings)}") - builder.appendLine() - - formatRelaySection( - context.getString(R.string.public_home_section), - context.getString(R.string.public_home_section_explainer), - homeRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.public_notif_section), - context.getString(R.string.public_notif_section_explainer), - notifRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.private_inbox_section), - context.getString(R.string.private_inbox_section_explainer), - dmRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.private_outbox_section), - context.getString(R.string.private_outbox_section_explainer), - privateOutboxRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.proxy_section), - context.getString(R.string.proxy_section_explainer), - proxyRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.broadcast_section), - context.getString(R.string.broadcast_section_explainer), - broadcastRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.indexer_section), - context.getString(R.string.indexer_section_explainer), - indexerRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.search_section), - context.getString(R.string.search_section_explainer), - searchRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.local_section), - context.getString(R.string.local_section_explainer), - localRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.trusted_section), - context.getString(R.string.trusted_section_explainer), - trustedRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.favorite_section), - context.getString(R.string.favorite_section_explainer), - favoriteRelays, - builder, - ) - formatRelaySection( - context.getString(R.string.blocked_section), - context.getString(R.string.blocked_section_explainer), - blockedRelays, - builder, - ) - - return builder.toString().trimEnd() -} - -fun exportRelaySettings( - context: Context, - homeRelays: List, - notifRelays: List, - dmRelays: List, - privateOutboxRelays: List, - proxyRelays: List, - broadcastRelays: List, - indexerRelays: List, - searchRelays: List, - localRelays: List, - trustedRelays: List, - favoriteRelays: List, - blockedRelays: List, -) { - val text = - buildRelaySettingsText( - context, - homeRelays, - notifRelays, - dmRelays, - privateOutboxRelays, - proxyRelays, - broadcastRelays, - indexerRelays, - searchRelays, - localRelays, - trustedRelays, - favoriteRelays, - blockedRelays, - ) - - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, text) - putExtra(Intent.EXTRA_TITLE, context.getString(R.string.export_relay_settings)) - } - - val shareIntent = - Intent.createChooser( - sendIntent, - context.getString(R.string.export_relay_settings), - ) - context.startActivity(shareIntent) -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt new file mode 100644 index 0000000000..947ca078f5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common + +import android.content.Context +import android.content.Intent +import com.vitorpamplona.amethyst.R + +class RelayExporter( + val context: Context, +) { + fun export( + homeRelays: List, + notifRelays: List, + dmRelays: List, + privateOutboxRelays: List, + proxyRelays: List, + broadcastRelays: List, + indexerRelays: List, + searchRelays: List, + localRelays: List, + trustedRelays: List, + favoriteRelays: List, + blockedRelays: List, + ) { + val text = + buildExportText( + homeRelays, + notifRelays, + dmRelays, + privateOutboxRelays, + proxyRelays, + broadcastRelays, + indexerRelays, + searchRelays, + localRelays, + trustedRelays, + favoriteRelays, + blockedRelays, + ) + + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, text) + putExtra(Intent.EXTRA_TITLE, context.getString(R.string.export_relay_settings)) + } + + val shareIntent = + Intent.createChooser( + sendIntent, + context.getString(R.string.export_relay_settings), + ) + context.startActivity(shareIntent) + } + + fun buildExportText( + homeRelays: List, + notifRelays: List, + dmRelays: List, + privateOutboxRelays: List, + proxyRelays: List, + broadcastRelays: List, + indexerRelays: List, + searchRelays: List, + localRelays: List, + trustedRelays: List, + favoriteRelays: List, + blockedRelays: List, + ): String { + val builder = StringBuilder() + builder.appendLine("# ${context.getString(R.string.relay_settings)}") + builder.appendLine() + + formatSection(R.string.public_home_section, R.string.public_home_section_explainer, homeRelays, builder) + formatSection(R.string.public_notif_section, R.string.public_notif_section_explainer, notifRelays, builder) + formatSection(R.string.private_inbox_section, R.string.private_inbox_section_explainer, dmRelays, builder) + formatSection(R.string.private_outbox_section, R.string.private_outbox_section_explainer, privateOutboxRelays, builder) + formatSection(R.string.proxy_section, R.string.proxy_section_explainer, proxyRelays, builder) + formatSection(R.string.broadcast_section, R.string.broadcast_section_explainer, broadcastRelays, builder) + formatSection(R.string.indexer_section, R.string.indexer_section_explainer, indexerRelays, builder) + formatSection(R.string.search_section, R.string.search_section_explainer, searchRelays, builder) + formatSection(R.string.local_section, R.string.local_section_explainer, localRelays, builder) + formatSection(R.string.trusted_section, R.string.trusted_section_explainer, trustedRelays, builder) + formatSection(R.string.favorite_section, R.string.favorite_section_explainer, favoriteRelays, builder) + formatSection(R.string.blocked_section, R.string.blocked_section_explainer, blockedRelays, builder) + + return builder.toString().trimEnd() + } + + private fun formatSection( + titleRes: Int, + descriptionRes: Int, + relays: List, + builder: StringBuilder, + ) { + if (relays.isEmpty()) return + builder.appendLine("## ${context.getString(titleRes)}") + builder.appendLine("# ${context.getString(descriptionRes)}") + builder.appendLine() + relays.forEach { relay -> + builder.appendLine(relay.relay.url) + } + builder.appendLine() + } +} From 1966cc20345c06ed0a667c91c40bb9d866cc6a90 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 13:29:55 -0400 Subject: [PATCH 053/172] Bitcoin first then namecoin --- .../screen/loggedIn/settings/AllSettingsScreen.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index 5e9e200a48..732ef5fcd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -147,13 +147,6 @@ fun AllSettingsScreen( onClick = { nav.nav(Route.PrivacyOptions) }, ) HorizontalDivider() - SettingsNavigationRow( - title = R.string.namecoin_settings, - icon = Icons.Outlined.Security, - tint = tint, - onClick = { nav.nav(Route.NamecoinSettings) }, - ) - HorizontalDivider() SettingsNavigationRow( title = R.string.ots_explorer_settings, icon = Icons.Outlined.Search, @@ -161,6 +154,13 @@ fun AllSettingsScreen( onClick = { nav.nav(Route.OtsSettings) }, ) HorizontalDivider() + SettingsNavigationRow( + title = R.string.namecoin_settings, + icon = Icons.Outlined.Security, + tint = tint, + onClick = { nav.nav(Route.NamecoinSettings) }, + ) + HorizontalDivider() SettingsNavigationRow( title = R.string.ui_preferences, icon = Icons.Outlined.Settings, From 9ec8dc167c97bbb62253cb57538e249801285b33 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 13 Mar 2026 17:36:41 +0000 Subject: [PATCH 054/172] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hu-rHU/strings.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index d1cf8e235a..f385c1347e 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -928,6 +928,8 @@ Győződjön meg arról, hogy ezt a tranzakciót az aláíró-alkalmazás hitelesítette-e Nem található pénztárca a Lighning-számla kifizetéséhez (Hiba: %1$s). A Zap-ek használatához telepítsen egy Lightning-pénztárcát Nem található pénztárca a Lighning-számla kifizetéséhez. A Zap-ek használatához telepítsen egy Lightning-pénztárcát + Nem lehet megnyitni a Blossom-hivatkozásokat + Nem található Blossom-alkalmazás. Telepítsen egy helyi Blossom-alkalmazást a fájl megtekintéséhez Rejtett szavak Új szó vagy mondat elrejtése Profilkép @@ -1271,6 +1273,19 @@ Hashtag keresése: #%1$s Innentől NE fordítsa le Az itt látható nyelvek nem lesznek lefordítva. Az eltávolításához és az újbóli fordításhoz válasszon ki egy nyelvet. + Fordítás erre: + Válassza ki a nyelvet, amelyre a tartalmat le szeretné fordítani. + Nyelvi megjelenítési beállítások + Minden egyes a fordításhoz szükséges nyelvpár esetében válassza ki, melyik nyelvet szeretné elsőként megjeleníteni. + %1$s → %2$s + Nyelvek keresése + Nyelv hozzáadása + Nyelvi párok hozzáadása + Forrásnyelv + Célnyelv + Előbb a(z) %1$s megjelenítése + Még nincsenek nyelvi beállítások. Ezek automatikusan létrejönnek a fordítások elkészültével, de kézzel is hozzáadhatók. + Beállítás törlése Szüneteltetés Lejátszás Legördülő menü megnyitása From 00f2d46b53f3318dd15d6077afecba41f0ce7b0f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 13:45:22 -0400 Subject: [PATCH 055/172] Refines user status flow and isExpired running --- .../relayClient/reqCommand/user/UserObservers.kt | 14 +++++++++++--- .../model/nip38UserStatuses/UserStatusCache.kt | 15 +++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index 779e0206e4..cc46feab78 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -38,7 +38,6 @@ import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview @@ -501,7 +500,15 @@ fun observeUserStatuses( // Subscribe in the relay for changes in the metadata of this user. UserFinderFilterAssemblerSubscription(user, accountViewModel) - return user.statusState().statuses.collectAsStateWithLifecycle(persistentListOf()) + val flow = + remember(user) { + user.statusState().statuses.onStart { + user.statusState().removeExpired() + } + } + + @SuppressLint("StateFlowValueCalledInComposition") + return flow.collectAsStateWithLifecycle(user.statusState().statuses.value) } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) @@ -520,5 +527,6 @@ fun observeUserRelayIntoList( .flowOn(Dispatchers.IO) } - return flow.collectAsStateWithLifecycle(false) + @SuppressLint("StateFlowValueCalledInComposition") + return flow.collectAsStateWithLifecycle(relayUrl in accountViewModel.account.trustedRelays.flow.value) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt index 2298cc18ed..6f8ebac80e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusCache.kt @@ -62,12 +62,15 @@ class UserStatusCache : UserDependencies { } fun removeExpired() { - statuses.update { list -> - val filtered = list.filter { it.event?.isExpired() != true } - if (filtered.size != list.size) { - filtered.toImmutableList() - } else { - list + val hasExpired = statuses.value.any { it.event?.isExpired() == true } + if (hasExpired) { + statuses.update { list -> + val filtered = list.filter { it.event?.isExpired() != true } + if (filtered.size != list.size) { + filtered.toImmutableList() + } else { + list + } } } } From 0ad63bbfc11760b51527070c00e1de775b3594c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 18:24:05 +0000 Subject: [PATCH 056/172] feat: add ZIP/JSON relay export and format picker dropdown - Create RelayListCollection to bundle all relay lists with section metadata - Create RelayZipExporter that exports each relay category as a separate JSON file (array of relay URL strings) inside a zip archive, shared via FileProvider - Refactor RelayExporter to use RelayListCollection - Replace single export button with a dropdown menu letting users pick between text export and ZIP (JSON) export https://claude.ai/code/session_013PhuahpBFh6djVHzSsmNKM --- .../loggedIn/relays/AllRelayListScreen.kt | 79 +++++++++++----- .../loggedIn/relays/common/RelayExporter.kt | 73 +++------------ .../relays/common/RelayListCollection.kt | 61 +++++++++++++ .../relays/common/RelayZipExporter.kt | 89 +++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 2 + 5 files changed, 221 insertions(+), 83 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 7a0902a2b3..49cd1508ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -29,6 +29,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -39,6 +41,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -83,6 +88,8 @@ import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.SettingsCategoryFirstModifier import com.vitorpamplona.amethyst.ui.theme.SettingsCategorySpacingModifier import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter import com.vitorpamplona.amethyst.ui.theme.grayText @Composable @@ -183,32 +190,28 @@ fun MappedAllRelayListView( val context = LocalContext.current + val collection = + RelayListCollection( + homeRelays = homeFeedState, + notifRelays = notifFeedState, + dmRelays = dmFeedState, + privateOutboxRelays = privateOutboxFeedState, + proxyRelays = proxyRelays, + broadcastRelays = broadcastRelays, + indexerRelays = indexerRelays, + searchRelays = searchFeedState, + localRelays = localFeedState, + trustedRelays = trustedFeedState, + favoriteRelays = relayFeedsFeedState, + blockedRelays = blockedFeedState, + ) + Scaffold( topBar = { SavingTopBar( titleRes = R.string.relay_settings, additionalActions = { - IconButton(onClick = { - RelayExporter(context).export( - homeRelays = homeFeedState, - notifRelays = notifFeedState, - dmRelays = dmFeedState, - privateOutboxRelays = privateOutboxFeedState, - proxyRelays = proxyRelays, - broadcastRelays = broadcastRelays, - indexerRelays = indexerRelays, - searchRelays = searchFeedState, - localRelays = localFeedState, - trustedRelays = trustedFeedState, - favoriteRelays = relayFeedsFeedState, - blockedRelays = blockedFeedState, - ) - }) { - Icon( - imageVector = Icons.Default.Share, - contentDescription = stringRes(R.string.export_relay_settings), - ) - } + ExportDropdownMenu(context, collection) }, onCancel = { dmViewModel.clear() @@ -479,3 +482,37 @@ fun SettingsCategoryWithButton( } } +@Composable +fun ExportDropdownMenu( + context: android.content.Context, + collection: RelayListCollection, +) { + var expanded by remember { mutableStateOf(false) } + + IconButton(onClick = { expanded = true }) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringRes(R.string.export_relay_settings), + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + DropdownMenuItem( + text = { Text(stringRes(R.string.export_as_text)) }, + onClick = { + expanded = false + RelayExporter(context).export(collection) + }, + ) + DropdownMenuItem( + text = { Text(stringRes(R.string.export_as_zip)) }, + onClick = { + expanded = false + RelayZipExporter(context).export(collection) + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt index 947ca078f5..ff226f4c81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayExporter.kt @@ -27,35 +27,8 @@ import com.vitorpamplona.amethyst.R class RelayExporter( val context: Context, ) { - fun export( - homeRelays: List, - notifRelays: List, - dmRelays: List, - privateOutboxRelays: List, - proxyRelays: List, - broadcastRelays: List, - indexerRelays: List, - searchRelays: List, - localRelays: List, - trustedRelays: List, - favoriteRelays: List, - blockedRelays: List, - ) { - val text = - buildExportText( - homeRelays, - notifRelays, - dmRelays, - privateOutboxRelays, - proxyRelays, - broadcastRelays, - indexerRelays, - searchRelays, - localRelays, - trustedRelays, - favoriteRelays, - blockedRelays, - ) + fun export(collection: RelayListCollection) { + val text = buildExportText(collection) val sendIntent = Intent().apply { @@ -73,51 +46,27 @@ class RelayExporter( context.startActivity(shareIntent) } - fun buildExportText( - homeRelays: List, - notifRelays: List, - dmRelays: List, - privateOutboxRelays: List, - proxyRelays: List, - broadcastRelays: List, - indexerRelays: List, - searchRelays: List, - localRelays: List, - trustedRelays: List, - favoriteRelays: List, - blockedRelays: List, - ): String { + fun buildExportText(collection: RelayListCollection): String { val builder = StringBuilder() builder.appendLine("# ${context.getString(R.string.relay_settings)}") builder.appendLine() - formatSection(R.string.public_home_section, R.string.public_home_section_explainer, homeRelays, builder) - formatSection(R.string.public_notif_section, R.string.public_notif_section_explainer, notifRelays, builder) - formatSection(R.string.private_inbox_section, R.string.private_inbox_section_explainer, dmRelays, builder) - formatSection(R.string.private_outbox_section, R.string.private_outbox_section_explainer, privateOutboxRelays, builder) - formatSection(R.string.proxy_section, R.string.proxy_section_explainer, proxyRelays, builder) - formatSection(R.string.broadcast_section, R.string.broadcast_section_explainer, broadcastRelays, builder) - formatSection(R.string.indexer_section, R.string.indexer_section_explainer, indexerRelays, builder) - formatSection(R.string.search_section, R.string.search_section_explainer, searchRelays, builder) - formatSection(R.string.local_section, R.string.local_section_explainer, localRelays, builder) - formatSection(R.string.trusted_section, R.string.trusted_section_explainer, trustedRelays, builder) - formatSection(R.string.favorite_section, R.string.favorite_section_explainer, favoriteRelays, builder) - formatSection(R.string.blocked_section, R.string.blocked_section_explainer, blockedRelays, builder) + collection.sections().forEach { section -> + formatSection(section, builder) + } return builder.toString().trimEnd() } private fun formatSection( - titleRes: Int, - descriptionRes: Int, - relays: List, + section: RelaySection, builder: StringBuilder, ) { - if (relays.isEmpty()) return - builder.appendLine("## ${context.getString(titleRes)}") - builder.appendLine("# ${context.getString(descriptionRes)}") + if (section.relays.isEmpty()) return + builder.appendLine("## ${context.getString(section.titleRes)}") + builder.appendLine("# ${context.getString(section.descriptionRes)}") builder.appendLine() - relays.forEach { relay -> + section.relays.forEach { relay -> builder.appendLine(relay.relay.url) } builder.appendLine() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt new file mode 100644 index 0000000000..126f208a6a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayListCollection.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common + +import com.vitorpamplona.amethyst.R + +data class RelayListCollection( + val homeRelays: List, + val notifRelays: List, + val dmRelays: List, + val privateOutboxRelays: List, + val proxyRelays: List, + val broadcastRelays: List, + val indexerRelays: List, + val searchRelays: List, + val localRelays: List, + val trustedRelays: List, + val favoriteRelays: List, + val blockedRelays: List, +) { + fun sections(): List = + listOf( + RelaySection("home", R.string.public_home_section, R.string.public_home_section_explainer, homeRelays), + RelaySection("notifications", R.string.public_notif_section, R.string.public_notif_section_explainer, notifRelays), + RelaySection("private_inbox", R.string.private_inbox_section, R.string.private_inbox_section_explainer, dmRelays), + RelaySection("private_outbox", R.string.private_outbox_section, R.string.private_outbox_section_explainer, privateOutboxRelays), + RelaySection("proxy", R.string.proxy_section, R.string.proxy_section_explainer, proxyRelays), + RelaySection("broadcast", R.string.broadcast_section, R.string.broadcast_section_explainer, broadcastRelays), + RelaySection("indexer", R.string.indexer_section, R.string.indexer_section_explainer, indexerRelays), + RelaySection("search", R.string.search_section, R.string.search_section_explainer, searchRelays), + RelaySection("local", R.string.local_section, R.string.local_section_explainer, localRelays), + RelaySection("trusted", R.string.trusted_section, R.string.trusted_section_explainer, trustedRelays), + RelaySection("favorites", R.string.favorite_section, R.string.favorite_section_explainer, favoriteRelays), + RelaySection("blocked", R.string.blocked_section, R.string.blocked_section_explainer, blockedRelays), + ) +} + +data class RelaySection( + val fileName: String, + val titleRes: Int, + val descriptionRes: Int, + val relays: List, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt new file mode 100644 index 0000000000..d1613f5e30 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayZipExporter.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common + +import android.content.Context +import android.content.Intent +import androidx.core.content.FileProvider +import com.vitorpamplona.amethyst.R +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class RelayZipExporter( + val context: Context, +) { + fun export(collection: RelayListCollection) { + val zipFile = buildZipFile(collection) + + val uri = + FileProvider.getUriForFile( + context, + "${context.packageName}.provider", + zipFile, + ) + + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "application/zip" + putExtra(Intent.EXTRA_STREAM, uri) + putExtra(Intent.EXTRA_TITLE, context.getString(R.string.export_relay_settings)) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + val shareIntent = + Intent.createChooser( + sendIntent, + context.getString(R.string.export_relay_settings), + ) + context.startActivity(shareIntent) + } + + fun buildZipFile(collection: RelayListCollection): File { + val zipFile = File(context.cacheDir, "relay_settings.zip") + + ZipOutputStream(FileOutputStream(zipFile)).use { zip -> + collection.sections().forEach { section -> + if (section.relays.isNotEmpty()) { + val json = buildJsonArray(section.relays) + zip.putNextEntry(ZipEntry("${section.fileName}.json")) + zip.write(json.toByteArray()) + zip.closeEntry() + } + } + } + + return zipFile + } + + private fun buildJsonArray(relays: List): String { + val builder = StringBuilder() + builder.appendLine("[") + relays.forEachIndexed { index, relay -> + val comma = if (index < relays.size - 1) "," else "" + builder.appendLine(" \"${relay.relay.url}\"$comma") + } + builder.append("]") + return builder.toString() + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 02349fa0df..9270c95cbe 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1380,6 +1380,8 @@ Amethyst will never connect to these relays Export relay settings + Export as text + Export as ZIP (JSON) Zap the Devs! Your donation helps us make a difference. Every sat counts! From 9b58a1be1115bdc1c1d29df3ba53605952074eb0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 15:06:03 -0400 Subject: [PATCH 057/172] Fixing IO Dispatchers and scopes of choice. --- .../edits/PrivateStorageRelayListState.kt | 3 +- .../nip01UserMetadata/UserMetadataState.kt | 3 +- .../model/nip17Dms/DmRelayListState.kt | 3 +- .../blockedRelays/BlockedRelayListState.kt | 3 +- .../geohashLists/GeohashListState.kt | 3 +- .../hashtagLists/HashtagListState.kt | 3 +- .../indexerRelays/IndexerRelayListState.kt | 3 +- .../nip51Lists/muteList/MuteListState.kt | 3 +- .../relayFeeds/RelayFeedListState.kt | 3 +- .../searchRelays/SearchRelayListState.kt | 3 +- .../trustedRelays/TrustedRelayListState.kt | 3 +- .../nip65RelayList/Nip65RelayListState.kt | 3 +- .../nip72Communities/CommunityListState.kt | 3 +- .../nip78AppSpecific/AppSpecificState.kt | 3 +- .../NipA3PaymentTargetsState.kt | 3 +- .../TrustProviderListState.kt | 3 +- .../composable/mediaitem/MediaItemCache.kt | 54 ++++++++------- .../wavefront/FakeWaveformAnimation.kt | 7 +- .../composable/wavefront/PlayerExt.kt | 67 +++++++++++++++++++ .../playback/composable/wavefront/Waveform.kt | 16 +---- .../amethyst/ui/actions/EditPostViewModel.kt | 3 +- .../amethyst/ui/actions/NewMediaModel.kt | 2 +- .../ui/note/elements/DisplayReward.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 2 +- .../UserExternalIdentitiesViewModel.kt | 31 +++++---- .../model/emphChat/EphemeralChatListState.kt | 3 +- .../nip28PublicChats/PublicChatListState.kt | 3 +- 27 files changed, 142 insertions(+), 96 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/PlayerExt.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt index 12b38b3a21..80c35d16f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/edits/PrivateStorageRelayListState.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -94,7 +93,7 @@ class PrivateStorageRelayListState( settings.backupPrivateHomeRelayList?.let { event -> Log.d("AccountRegisterObservers", "Loading saved private home relay list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt index 27c295c5ca..7c7a1c235f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip01UserMetadata/UserMetadataState.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch class UserMetadataState( @@ -136,7 +135,7 @@ class UserMetadataState( Log.d("AccountRegisterObservers", "Loading saved user metadata ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } } // saves contact list for the next time. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt index ff40e726c7..324ddde21d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip17Dms/DmRelayListState.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -91,7 +90,7 @@ class DmRelayListState( settings.backupDMRelayList?.let { Log.d("AccountRegisterObservers", "Loading saved DM Relay List ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt index 4a8112ce29..f126d6461c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/blockedRelays/BlockedRelayListState.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -96,7 +95,7 @@ class BlockedRelayListState( settings.backupBlockedRelayList?.let { Log.d("AccountRegisterObservers", "Loading saved Blocked relay list ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } } scope.launch(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt index 58d08357eb..669eb58989 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/geohashLists/GeohashListState.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -109,7 +108,7 @@ class GeohashListState( settings.backupGeohashList?.let { event -> Log.d("AccountRegisterObservers", "Loading saved Geohash list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt index 9bee99dce4..7d5d490ecd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/hashtagLists/HashtagListState.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -109,7 +108,7 @@ class HashtagListState( settings.backupHashtagList?.let { event -> Log.d("AccountRegisterObservers", "Loading saved Hashtag list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt index 7a23ce7eea..aef44173cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/indexerRelays/IndexerRelayListState.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -106,7 +105,7 @@ class IndexerRelayListState( settings.backupIndexRelayList?.let { Log.d("AccountRegisterObservers", "Loading saved index relay list ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } } scope.launch(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt index ff1df0a048..f71705966b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/muteList/MuteListState.kt @@ -33,7 +33,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -144,7 +143,7 @@ class MuteListState( settings.backupMuteList?.let { event -> Log.d("AccountRegisterObservers", "Loading saved mute list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt index 1ba8389e2f..de9b52c16d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/relayFeeds/RelayFeedListState.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -118,7 +117,7 @@ class RelayFeedListState( settings.backupRelayFeedsList?.let { Log.d("AccountRegisterObservers", "Loading saved relay feeds list ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } } scope.launch(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt index 1f823adbc9..5824016c32 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/searchRelays/SearchRelayListState.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -106,7 +105,7 @@ class SearchRelayListState( settings.backupSearchRelayList?.let { Log.d("AccountRegisterObservers", "Loading saved search relay list ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) } } scope.launch(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt index 995c41cd61..bce1af25d4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/trustedRelays/TrustedRelayListState.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -93,7 +92,7 @@ class TrustedRelayListState( settings.backupTrustedRelayList?.let { Log.d("AccountRegisterObservers", "Loading saved Trusted relay list ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) } + scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) } } scope.launch(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt index 98dc4727b6..c0a913fd6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip65RelayList/Nip65RelayListState.kt @@ -33,7 +33,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -147,7 +146,7 @@ class Nip65RelayListState( settings.backupNIP65RelayList?.let { Log.d("AccountRegisterObservers", "Loading saved nip65 relay list ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) } + scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) } } scope.launch(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt index 4f249e3fbb..a4fd1285a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip72Communities/CommunityListState.kt @@ -34,7 +34,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -158,7 +157,7 @@ class CommunityListState( settings.backupCommunityList?.let { event -> Log.d("AccountRegisterObservers", "Loading saved Community list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt index 52270c6c71..d68d64f9c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip78AppSpecific/AppSpecificState.kt @@ -31,7 +31,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlin.coroutines.cancellation.CancellationException @@ -68,7 +67,7 @@ class AppSpecificState( settings.backupAppSpecificData?.let { event -> Log.d("AccountRegisterObservers", "Loading saved app specific data ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(event) try { val decrypted = signer.decrypt(event.content, event.pubKey) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt index 6cf864ff87..58bb305149 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nipA3PaymentTargets/NipA3PaymentTargetsState.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.utils.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -49,7 +48,7 @@ class NipA3PaymentTargetsState( settings.backupNipA3PaymentTargets?.let { Log.d("AccountRegisterObservers", "Loading saved nipA3 Payment targets ${it.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) } + scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(it) } } scope.launch(Dispatchers.IO) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt index 70d910c600..be345e5953 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/trustedAssertions/TrustProviderListState.kt @@ -33,7 +33,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -117,7 +116,7 @@ class TrustProviderListState( settings.backupTrustProviderList?.let { event -> Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt index f029eaf16e..c1cb800def 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/mediaitem/MediaItemCache.kt @@ -25,33 +25,37 @@ import androidx.core.net.toUri import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlin.coroutines.cancellation.CancellationException class MediaItemCache : GenericBaseCache(20) { override suspend fun compute(key: MediaItemData): LoadedMediaItem = - LoadedMediaItem( - key, - MediaItem - .Builder() - .setMediaId(key.videoUri) - .setUri(key.videoUri) - .setMediaMetadata( - MediaMetadata - .Builder() - .setArtist(key.authorName?.ifBlank { null }) - .setTitle(key.title?.ifBlank { null } ?: key.videoUri) - .setExtras( - Bundle().apply { - putString("callbackUri", key.callbackUri) - }, - ).setArtworkUri( - try { - key.artworkUri?.toUri() - } catch (e: Exception) { - if (e is CancellationException) throw e - null - }, - ).build(), - ).build(), - ) + withContext(Dispatchers.IO) { + LoadedMediaItem( + key, + MediaItem + .Builder() + .setMediaId(key.videoUri) + .setUri(key.videoUri) + .setMediaMetadata( + MediaMetadata + .Builder() + .setArtist(key.authorName?.ifBlank { null }) + .setTitle(key.title?.ifBlank { null } ?: key.videoUri) + .setExtras( + Bundle().apply { + putString("callbackUri", key.callbackUri) + }, + ).setArtworkUri( + try { + key.artworkUri?.toUri() + } catch (e: Exception) { + if (e is CancellationException) throw e + null + }, + ).build(), + ).build(), + ) + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt index 2e05aa6adb..b46a2ea14c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/FakeWaveformAnimation.kt @@ -44,9 +44,11 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.Player import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.onStart import kotlin.math.sin @@ -79,7 +81,7 @@ fun FakeWaveformAnimation( } LaunchedEffect(key1 = restartFlow.intValue) { - pollCurrentPosition(mediaControllerState.controller).collect { value -> + mediaControllerState.controller.pollCurrentPositionFlow().collect { value -> waveformProgress.floatValue = (value % 5000.0f) / 5000.0f } } @@ -93,7 +95,8 @@ fun pollCurrentPosition(controller: Player) = } }.onStart { emit(controller.currentPosition) - }.conflate() + }.flowOn(Dispatchers.IO) + .conflate() @Preview @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/PlayerExt.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/PlayerExt.kt new file mode 100644 index 0000000000..51a7a30ab2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/PlayerExt.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.service.playback.composable.wavefront + +import androidx.media3.common.Player +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +suspend fun Player.completionRatio() = withContext(Dispatchers.Main) { currentPosition / duration.toFloat() } + +suspend fun Player.positionDuration() = withContext(Dispatchers.Main) { PositionDuration(currentPosition, duration) } + +class PositionDuration( + val position: Long, + val duration: Long, +) { + fun finished() = position > duration + + fun ratio() = position / (duration.toFloat()) +} + +fun Player.pollCurrentRelativePositionFlow() = + flow { + do { + delay(100) + val ratio = positionDuration() + emit(ratio.ratio()) + } while (!ratio.finished()) + }.onStart { + emit(completionRatio()) + }.flowOn(Dispatchers.IO) + .conflate() + +fun Player.pollCurrentPositionFlow() = + flow { + do { + delay(100) + val ratio = positionDuration() + emit(ratio.position) + } while (!ratio.finished()) + }.onStart { + emit(withContext(Dispatchers.Main) { currentPosition }) + }.flowOn(Dispatchers.IO) + .conflate() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt index 68fd893c4e..be8bdd5d03 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/playback/composable/wavefront/Waveform.kt @@ -39,10 +39,6 @@ import com.linc.audiowaveform.infiniteLinearGradient import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.WaveformData import com.vitorpamplona.amethyst.ui.components.AudioWaveformReadOnly -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onStart @Composable fun Waveform( @@ -74,20 +70,10 @@ fun Waveform( } LaunchedEffect(key1 = restartFlow.intValue) { - pollCurrentRelativePosition(mediaControllerState.controller).collect { value -> waveformProgress.floatValue = value } + mediaControllerState.controller.pollCurrentRelativePositionFlow().collect { value -> waveformProgress.floatValue = value } } } -fun pollCurrentRelativePosition(controller: Player) = - flow { - while (controller.currentPosition <= controller.duration) { - emit(controller.currentPosition / controller.duration.toFloat()) - delay(100) - } - }.onStart { - emit(controller.currentPosition / controller.duration.toFloat()) - }.conflate() - @Composable fun DrawWaveform( waveform: WaveformData, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 00ce6b4759..1c9d57d603 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -61,6 +61,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Stable @@ -175,7 +176,7 @@ open class EditPostViewModel : ViewModel() { onError: (String, String) -> Unit, context: Context, ) { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { val myAccount = account val myMultiOrchestrator = multiOrchestrator ?: return@launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt index 00ea735d0a..97ac2a6a53 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewMediaModel.kt @@ -98,7 +98,7 @@ open class NewMediaModel : ViewModel() { onSucess: () -> Unit, onError: (String, String) -> Unit, ) { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { val myAccount = account ?: return@launch val serverToUse = selectedServer ?: return@launch diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt index 54cde53ca5..f13cb18fac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DisplayReward.kt @@ -173,7 +173,7 @@ class AddBountyAmountViewModel : ViewModel() { val newValue = nextAmount.text.trim().toBigDecimalOrNull() if (newValue != null) { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { account?.let { myAccount -> bounty?.let { bountyInner -> myAccount.sendAddBounty( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 5eb51fd1ae..c49e1ab29a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -593,7 +593,7 @@ open class ShortNotePostViewModel : val (event, relays, extras) = accountViewModel.account.createPostEvent(template, extraNotesToBroadcast) // Launch broadcast in background - don't wait for completion - accountViewModel.viewModelScope.launch { + accountViewModel.viewModelScope.launch(Dispatchers.IO) { accountViewModel.broadcastTracker.trackBroadcast( event = event, relays = relays, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt index a091fefc15..96139b4df6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/identity/UserExternalIdentitiesViewModel.kt @@ -27,32 +27,35 @@ import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip39ExtIdentities.ExternalIdentitiesEvent -import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn @Stable class UserExternalIdentitiesViewModel( val user: User, ) : ViewModel() { - private val _identities = MutableStateFlow>(emptyList()) - val identities = _identities.asStateFlow() - private val note = LocalCache.getOrCreateAddressableNote( ExternalIdentitiesEvent.createAddress(user.pubkeyHex), ) - init { - viewModelScope.launch { - note.flow().metadata.stateFlow.collect { state -> + val identities = + note + .flow() + .metadata.stateFlow + .map { state -> val event = state.note.event as? ExternalIdentitiesEvent - _identities.value = event?.identityClaims() ?: emptyList() - } - } - } + event?.identityClaims() ?: emptyList() + }.flowOn(Dispatchers.IO) + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList(), + ) class Factory( val user: User, diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt index 3fc3a5e9e8..11f888e9a1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -117,7 +116,7 @@ class EphemeralChatListState( settings.ephemeralChatList()?.let { event -> Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(event) } } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt index c0890ffde4..3b981be8a2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn @@ -132,7 +131,7 @@ class PublicChatListState( settings.channelList()?.let { event -> Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(Dispatchers.IO) { + scope.launch(Dispatchers.IO) { cache.justConsumeMyOwnEvent(event) } } From 83394132627ef4e15f68c9239b2ce526f543a030 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 15:20:01 -0400 Subject: [PATCH 058/172] Fixes copilot recommendation --- .../java/com/vitorpamplona/amethyst/AppModules.kt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt index e9e7c6f6fa..cf9256dc47 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/AppModules.kt @@ -129,13 +129,6 @@ class AppModules( OtsSharedPreferences(appContext, applicationIOScope) } - init { - applicationIOScope.launch { - // Eagerly initialize OtsSharedPreferences off the main thread - otsPrefs - } - } - // App services that should be run as soon as there are subscribers to their flows val locationManager = LocationState(appContext, applicationIOScope) val connManager = ConnectivityManager(appContext, applicationIOScope) @@ -406,6 +399,11 @@ class AppModules( delay(3000) videoCache } + + applicationIOScope.launch { + // Eagerly initialize OtsSharedPreferences off the main thread + otsPrefs + } } fun terminate(appContext: Context) { From e51967923ba2ea4f49dc51116f5d1796f83069bd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 15:20:11 -0400 Subject: [PATCH 059/172] adds today's work to change log --- CHANGELOG.md | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd274bcac6..7c4ac7c405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ Adds support for NIP-66 Relay Monitor and discovery support to Quartz Adds support for NIP-A3 Payment targets (PayTo: 10133) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 -Adds support for BUD-10 "Blossom:" URIs +Adds support for BUD-10 "Blossom:" URIs in images, audios, videos, and documents. Adds support for Namecoin .bit urls to NIP-05 - Adds choice of ElectrumX server to resolve namecoins. @@ -46,13 +46,15 @@ Adds support to upload Documents to all new post screens. Content warning improvements: - Adds optional description field for sensitive content warnings in new posts. -- Displays additional information on warning composables. +- Displays additional information on warning composables Redesigns and reorganizes Setting pages - Consolidate drawer settings into a single Settings hub screen - Redesigns Zap Amount and NWC setup screens - Redesigns Custom zap amount screens -- Add reactions row settings (enable/disable, order, show/hide counters) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 +- Adds brand new Translation Settings screen +- Adds blockchain explorer settings page for OTS verification +- Adds reactions row settings (enable/disable, order, show/hide counters) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 - Tapping on Zap without any pre-configured amount opens the custom dialog URL/URI parser rewrite in Kotlin multiplatform (KMP) @@ -63,14 +65,13 @@ URL/URI parser rewrite in Kotlin multiplatform (KMP) Minimizes parent thread rendering in quoted notes by @npub1aeh2zw4elewy5682lxc6xnlqzjnxksq303gwu2npfaxd49vmde6qcq4nwx -Fixes bug on Show More calculations for very long texts without spaces - Relay Management: - Adds relay search tooltip when adding relays - Adds the list of keys using each relay to the relay information - Adds active subscriptions and outbox event in the queue to relay information - Adds a complete list of event kind names to the subscription card to relay information - Tracks and displays connection success rate on relay settings +- Add relay settings export functionality Search fixes - Breaks the search filter into two subscriptions to prioritize Metadata without punishing content. @@ -117,9 +118,14 @@ Fixes: - Location permission watcher moved outside screens to avoid recreation - Solves the sorting contract crash on search by precaching all values before sorting users. - Fixes lingering relay connections from loading follows outbox's settings. +- Enhance NIP-38 user status display with emoji support and metadata tags +- Fixes bug on Show More calculations for very long texts without spaces +- Fixing IO Dispatchers and coroutine scopes of choice +- Fixes anySync parallel operation that was returning the first result, not the first positive "any". AI: - Add SKILL.md for AI agent customization +- Add settings and hooks to setup Android Development for the agent Defaults: - Switches wss://nostr.band to wss://antiprimal.net, wss://relay.ditto.pub on app defaults @@ -129,15 +135,16 @@ Defaults: Adds complete support for iOS to Quartz by @npub1a3tx8wcrt789skl6gg7rqwj4wey0j53eesr4z6asd4h4jwrd62jq0wkq4k - Provide implementation for Rfc3986 on iOS, using the Swift Rfc3986UriBridge. -- Provide implementation for LargeCache, using a CacheMap. +- Provide implementation for LargeCache, using a CacheMap - Provide implementation for fastFindURLs() - Provide implementation for makeAbsoluteIfRelativeUrl() in ServerInfoParser.ios.kt -- Provide implementation for UrlEncoder. -- Provide implementation for UnicodeNormalizer. +- Provide implementation for UrlEncoder +- Provide implementation for UnicodeNormalizer - Provide implementation for GZip compression/decompression. Some small fixes in URLs.ios.kt -- Provide implementation for AESCBC. -- Provide implementation for AESGCM. -- Provide implementation for DigestInstance. +- Provide implementation for AESCBC +- Provide implementation for AESGCM +- Provide implementation for DigestInstance +- Provide implementation for LibSodium - Adds a reqUntilEoseAsFlow extension to the Nostr Client Amethyst Desktop by @npub12cfje6nl2nuxplcqfvhg7ljt89fmpj0n0fd24zxsukja5qm9wmtqd7y76c From d281c2366c0a5dda03e4b1ef4e24a99bfe7a7a3e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 16:04:46 -0400 Subject: [PATCH 060/172] Removes the need to pass context down to a composable --- .../ui/screen/loggedIn/relays/AllRelayListScreen.kt | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index a6f467199d..6e420e6010 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -188,8 +188,6 @@ fun MappedAllRelayListView( val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle() - val context = LocalContext.current - val collection = RelayListCollection( homeRelays = homeFeedState, @@ -211,7 +209,7 @@ fun MappedAllRelayListView( SavingTopBar( titleRes = R.string.relay_settings, additionalActions = { - ExportDropdownMenu(context, collection) + ExportDropdownMenu(collection) }, onCancel = { dmViewModel.clear() @@ -483,10 +481,7 @@ fun SettingsCategoryWithButton( } @Composable -fun ExportDropdownMenu( - context: android.content.Context, - collection: RelayListCollection, -) { +fun ExportDropdownMenu(collection: RelayListCollection) { var expanded by remember { mutableStateOf(false) } IconButton(onClick = { expanded = true }) { @@ -500,6 +495,7 @@ fun ExportDropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, ) { + val context = LocalContext.current DropdownMenuItem( text = { Text(stringRes(R.string.export_as_text)) }, onClick = { From d81609165e31e5ce865adca9a4c11a0b7bbb7923 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 16:28:50 -0400 Subject: [PATCH 061/172] Small refactoring of the relay list screen --- .../loggedIn/relays/AllRelayListScreen.kt | 71 +++++++++---------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index 6e420e6010..e2b79d62cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -172,7 +172,7 @@ fun MappedAllRelayListView( proxyViewModel: ProxyRelayListViewModel, relayFeedsViewModel: RelayFeedsListViewModel, accountViewModel: AccountViewModel, - newNav: INav, + nav: INav, ) { val dmFeedState by dmViewModel.relays.collectAsStateWithLifecycle() val homeFeedState by nip65ViewModel.homeRelays.collectAsStateWithLifecycle() @@ -188,28 +188,27 @@ fun MappedAllRelayListView( val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle() - val collection = - RelayListCollection( - homeRelays = homeFeedState, - notifRelays = notifFeedState, - dmRelays = dmFeedState, - privateOutboxRelays = privateOutboxFeedState, - proxyRelays = proxyRelays, - broadcastRelays = broadcastRelays, - indexerRelays = indexerRelays, - searchRelays = searchFeedState, - localRelays = localFeedState, - trustedRelays = trustedFeedState, - favoriteRelays = relayFeedsFeedState, - blockedRelays = blockedFeedState, - ) - Scaffold( topBar = { SavingTopBar( titleRes = R.string.relay_settings, additionalActions = { - ExportDropdownMenu(collection) + ExportDropdownMenu { + RelayListCollection( + homeRelays = homeFeedState, + notifRelays = notifFeedState, + dmRelays = dmFeedState, + privateOutboxRelays = privateOutboxFeedState, + proxyRelays = proxyRelays, + broadcastRelays = broadcastRelays, + indexerRelays = indexerRelays, + searchRelays = searchFeedState, + localRelays = localFeedState, + trustedRelays = trustedFeedState, + favoriteRelays = relayFeedsFeedState, + blockedRelays = blockedFeedState, + ) + } }, onCancel = { dmViewModel.clear() @@ -223,7 +222,7 @@ fun MappedAllRelayListView( indexerViewModel.clear() proxyViewModel.clear() relayFeedsViewModel.clear() - newNav.popBack() + nav.popBack() }, onPost = { dmViewModel.create() @@ -237,7 +236,7 @@ fun MappedAllRelayListView( indexerViewModel.create() proxyViewModel.create() relayFeedsViewModel.create() - newNav.popBack() + nav.popBack() }, ) }, @@ -262,7 +261,7 @@ fun MappedAllRelayListView( SettingsCategoryFirstModifier, ) } - renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, newNav) + renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav) item { SettingsCategory( @@ -271,7 +270,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, newNav) + renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav) item { SettingsCategoryWithButton( @@ -283,7 +282,7 @@ fun MappedAllRelayListView( }, ) } - renderDMItems(dmFeedState, dmViewModel, accountViewModel, newNav) + renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav) item { SettingsCategory( @@ -292,7 +291,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, newNav) + renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav) item { SettingsCategory( @@ -301,7 +300,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, newNav) + renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav) item { SettingsCategory( @@ -310,7 +309,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, newNav) + renderBroadcastItems(broadcastRelays, broadcastViewModel, accountViewModel, nav) item { SettingsCategoryWithButton( @@ -321,7 +320,7 @@ fun MappedAllRelayListView( ResetIndexerRelays(indexerViewModel) } } - renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, newNav) + renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav) item { SettingsCategoryWithButton( @@ -332,7 +331,7 @@ fun MappedAllRelayListView( ResetSearchRelays(searchViewModel) } } - renderSearchItems(searchFeedState, searchViewModel, accountViewModel, newNav) + renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav) item { SettingsCategory( @@ -341,7 +340,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderLocalItems(localFeedState, localViewModel, accountViewModel, newNav) + renderLocalItems(localFeedState, localViewModel, accountViewModel, nav) item { SettingsCategory( @@ -350,7 +349,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderTrustedItems(trustedFeedState, trustedViewModel, accountViewModel, newNav) + renderTrustedItems(trustedFeedState, trustedViewModel, accountViewModel, nav) item { SettingsCategory( @@ -359,7 +358,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderRelayFeedsItems(relayFeedsFeedState, relayFeedsViewModel, accountViewModel, newNav) + renderRelayFeedsItems(relayFeedsFeedState, relayFeedsViewModel, accountViewModel, nav) item { SettingsCategory( @@ -368,7 +367,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderBlockedItems(blockedFeedState, blockedViewModel, accountViewModel, newNav) + renderBlockedItems(blockedFeedState, blockedViewModel, accountViewModel, nav) item { SettingsCategory( @@ -377,7 +376,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderConnectedItems(connectedRelays, connectedViewModel, accountViewModel, newNav) + renderConnectedItems(connectedRelays, connectedViewModel, accountViewModel, nav) } } } @@ -481,7 +480,7 @@ fun SettingsCategoryWithButton( } @Composable -fun ExportDropdownMenu(collection: RelayListCollection) { +fun ExportDropdownMenu(collection: () -> RelayListCollection) { var expanded by remember { mutableStateOf(false) } IconButton(onClick = { expanded = true }) { @@ -500,14 +499,14 @@ fun ExportDropdownMenu(collection: RelayListCollection) { text = { Text(stringRes(R.string.export_as_text)) }, onClick = { expanded = false - RelayExporter(context).export(collection) + RelayExporter(context).export(collection()) }, ) DropdownMenuItem( text = { Text(stringRes(R.string.export_as_zip)) }, onClick = { expanded = false - RelayZipExporter(context).export(collection) + RelayZipExporter(context).export(collection()) }, ) } From 42fdedf23b5b6a2195695865338a1dce37149719 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 17:03:24 -0400 Subject: [PATCH 062/172] fixes spotless --- .../ui/note/creators/expiration/ExpirationDatePicker.kt | 4 +--- .../amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt | 4 ++-- .../ui/screen/loggedIn/home/ShortNotePostViewModel.kt | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt index 5dcd18b0a0..19aeca57f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt @@ -81,9 +81,7 @@ fun ExpirationDatePicker(model: ShortNotePostViewModel) { yearRange = currentTime.year..2050, selectableDates = object : SelectableDates { - override fun isSelectableDate(utcTimeMillis: Long): Boolean { - return utcTimeMillis >= System.currentTimeMillis() - 86400000 - } + override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= System.currentTimeMillis() - 86400000 }, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 6432d21691..8f799892e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -78,10 +78,10 @@ import com.vitorpamplona.amethyst.ui.note.BaseUserPicture import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensitivityExplainer import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton -import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton -import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 9d013613db..ec7447f9b8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -97,8 +97,8 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitiveOrNSFW -import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetupLnAddress import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup From c4c73a2bb700d60c95e8da404df9259e0309d8e9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 18:05:59 -0400 Subject: [PATCH 063/172] Adds expirations in all new post screens. Adds expirations in the feed and chats. --- .../amethyst/ui/note/NoteCompose.kt | 39 ++++++++++++++++ .../expiration/ExpirationDatePicker.kt | 3 +- .../note/creators/expiration/IExpiration.kt | 28 +++++++++++ .../nip22Comments/CommentPostViewModel.kt | 26 ++++++++++- .../nip22Comments/GenericCommentPostScreen.kt | 15 ++++++ .../loggedIn/chats/feed/ChatMessageCompose.kt | 1 + .../screen/loggedIn/chats/feed/ChatTimeAgo.kt | 46 +++++++++++++++++++ .../privateDM/send/ChatNewMessageViewModel.kt | 38 ++++++++++----- .../chats/privateDM/send/NewGroupDMScreen.kt | 10 ++++ .../send/PrivateMessageEditFieldRow.kt | 21 ++++----- .../send/ChannelNewMessageViewModel.kt | 31 ++++++++++++- .../nip99Classifieds/NewProductScreen.kt | 15 ++++++ .../nip99Classifieds/NewProductViewModel.kt | 25 +++++++++- .../loggedIn/home/ShortNotePostViewModel.kt | 6 ++- .../publicMessages/NewPublicMessageScreen.kt | 15 ++++++ .../NewPublicMessageViewModel.kt | 25 +++++++++- .../loggedIn/threadview/ThreadFeedView.kt | 3 ++ amethyst/src/main/res/values/strings.xml | 1 + 18 files changed, 316 insertions(+), 32 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index e85bb5caf5..42ab09d74f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -30,6 +30,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text @@ -47,6 +51,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -201,6 +206,7 @@ import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentCommentEvent import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip50Search.SearchRelayListEvent import com.vitorpamplona.quartz.nip51Lists.PinListEvent import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent @@ -1344,6 +1350,26 @@ fun SecondUserInfoRow( } } +@Composable +fun DisplayExpiration(expirationDate: Long) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = Icons.Outlined.Timer, + contentDescription = stringRes(R.string.expiration_date_label), + modifier = Modifier.size(15.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) + val context = LocalContext.current + Text( + text = timeAheadNoDot(expirationDate, context), + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + ) + } +} + @Composable fun DisplayOtsIfInOriginal( note: Note, @@ -1440,6 +1466,8 @@ fun FirstUserInfoRow( DisplayDraft() } + Expiration(baseNote) + TimeAgo(baseNote) if (moreOptions == null) { @@ -1450,6 +1478,17 @@ fun FirstUserInfoRow( } } +@Composable +fun Expiration(note: Note) { + val event = note.event + if (event != null) { + val expires = remember(event) { event.expiration() } + if (expires != null) { + DisplayExpiration(expires) + } + } +} + @Composable fun CheckAndDisplayEditStatus(editState: State>) { if (editState.value is GenericLoadable.Loaded) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt index 19aeca57f2..967f410586 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/ExpirationDatePicker.kt @@ -58,7 +58,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot -import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.placeholderText @@ -69,7 +68,7 @@ import java.time.ZoneOffset @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ExpirationDatePicker(model: ShortNotePostViewModel) { +fun ExpirationDatePicker(model: IExpiration) { var showDatePicker by remember { mutableStateOf(false) } var showTimePicker by remember { mutableStateOf(false) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt new file mode 100644 index 0000000000..6d3a77fdbf --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/expiration/IExpiration.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.creators.expiration + +import androidx.compose.runtime.Stable + +@Stable +interface IExpiration { + var expirationDate: Long +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index a885e3ce1b..c60b0a42e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -84,6 +86,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -101,6 +104,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.mimeType import com.vitorpamplona.quartz.nip94FileMetadata.originalHash import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow @@ -113,7 +117,8 @@ open class CommentPostViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() init { @@ -169,6 +174,10 @@ open class CommentPostViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -274,6 +283,10 @@ open class CommentPostViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val zapraiser = draftEvent.zapraiserAmount() wantsZapraiser = zapraiser != null zapRaiserAmount.value = null @@ -363,6 +376,7 @@ open class CommentPostViewModel : val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null val replyingTo = replyingTo val replyingToEvent = replyingTo?.event @@ -399,6 +413,7 @@ open class CommentPostViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -419,6 +434,7 @@ open class CommentPostViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -703,6 +719,14 @@ open class CommentPostViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + override fun locationFlow(): StateFlow { if (location == null) { location = locationManager().geohashStateFlow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 4eae653ac4..ed922a496f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -61,6 +61,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensiti import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton @@ -262,6 +264,15 @@ private fun GenericCommentPostBody( } } + if (postViewModel.wantsExpirationDate) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + ExpirationDatePicker(postViewModel) + } + } + if (postViewModel.wantsToAddGeoHash) { Row( verticalAlignment = CenterVertically, @@ -420,6 +431,10 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) { postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt index ffef3d53a0..3aeb9d13ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatMessageCompose.kt @@ -204,6 +204,7 @@ fun NormalChatNote( ) { IncognitoBadge(note) ChatTimeAgo(note) + ChatExpiration(note) RelayBadgesHorizontal(note, accountViewModel, nav = nav) Spacer(modifier = DoubleHorzSpacer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt index e53a80841b..cab7cf7521 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/feed/ChatTimeAgo.kt @@ -20,16 +20,29 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Timer +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.note.timeAgoShort +import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip40Expiration.expiration @Composable fun ChatTimeAgo(baseNote: Note) { @@ -43,3 +56,36 @@ fun ChatTimeAgo(baseNote: Note) { maxLines = 1, ) } + +@Composable +fun ChatExpiration(note: Note) { + val event = note.event + if (event != null) { + val expires = remember(event) { event.expiration() } + if (expires != null) { + ChatDisplayExpiration(expires) + } + } +} + +@Composable +fun ChatDisplayExpiration(expirationDate: Long) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = StdHorzSpacer) + Icon( + imageVector = Icons.Outlined.Timer, + contentDescription = stringRes(R.string.expiration_date_label), + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) + val context = LocalContext.current + Text( + text = timeAheadNoDot(expirationDate, context), + color = MaterialTheme.colorScheme.placeholderText, + fontSize = Font12SP, + maxLines = 1, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 61613f21f1..747920b86f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -42,6 +43,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -104,7 +106,8 @@ class ChatNewMessageViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() lateinit var accountViewModel: AccountViewModel @@ -163,6 +166,10 @@ class ChatNewMessageViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -172,8 +179,6 @@ class ChatNewMessageViewModel : var wantsZapraiser by mutableStateOf(false) override var zapRaiserAmount = mutableStateOf(null) - var expirationDays by mutableStateOf(null) - // NIP17 Wrapped DMs / Group messages var nip17 by mutableStateOf(false) @@ -274,7 +279,8 @@ class ChatNewMessageViewModel : } fun loadExpiration(expirationDays: Int) { - this.expirationDays = expirationDays + this.wantsExpirationDate = true + this.expirationDate = TimeUtils.now() + expirationDays * 86400L } private fun loadFromDraft(draft: Note) { @@ -296,6 +302,10 @@ class ChatNewMessageViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null @@ -348,8 +358,6 @@ class ChatNewMessageViewModel : } urlPreviews.update(message) - expirationDays = draftEvent.expiration()?.let { (it / 86_400).toInt() } - iMetaAttachments.addAll(draftEvent.imetas()) requiresNIP17 = draftEvent is NIP17Group @@ -444,11 +452,10 @@ class ChatNewMessageViewModel : val message = message.text val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null - val expiration = expirationDays?.let { TimeUtils.now() + it.toLong() * 86_400 } - if (nip17 || room.users.size > 1 || replyTo.value?.event is NIP17Group) { val replyHint = replyTo.value?.toEventHint() @@ -463,7 +470,7 @@ class ChatNewMessageViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } - expiration?.let { expiration(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -478,7 +485,7 @@ class ChatNewMessageViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } - expiration?.let { expiration(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -501,7 +508,7 @@ class ChatNewMessageViewModel : replyingTo = replyTo.value?.toEventHint(), signer = accountViewModel.account.signer, ) { - expiration?.let { expiration(it) } + localExpirationDate?.let { expiration(it) } } if (draftTag != null) { @@ -533,7 +540,6 @@ class ChatNewMessageViewModel : subject = TextFieldValue("") replyTo.value = null - expirationDays = null wantsInvoice = false wantsZapraiser = false @@ -742,5 +748,13 @@ class ChatNewMessageViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + override fun locationManager(): LocationState = Amethyst.instance.locationManager } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index c10ac9f862..3a2f1f8599 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -97,6 +97,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensiti import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton @@ -244,6 +246,10 @@ fun GroupDMScreenContent( ) } + if (postViewModel.wantsExpirationDate) { + ExpirationDatePicker(postViewModel) + } + if (postViewModel.wantsToAddGeoHash) { LocationAsHash(postViewModel) } @@ -458,6 +464,10 @@ private fun BottomRowActions( postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index db52197003..2615ac2ec6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send +import android.R.attr.maxLines import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -33,20 +34,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation -import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.ShowUserSuggestionList +import com.vitorpamplona.amethyst.ui.note.timeAheadNoDot import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.RoomChatFileUploadDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplyingToNote @@ -134,10 +135,14 @@ fun PrivateMessageEditFieldRow( ) } - channelScreenModel.expirationDays?.let { + if (channelScreenModel.wantsExpirationDate) { Row(Modifier.fillMaxWidth().padding(vertical = 2.dp), horizontalArrangement = Arrangement.Center) { + val context = LocalContext.current Text( - stringResource(R.string.this_message_will_disappear_in_days, it), + stringRes( + R.string.this_message_will_disappear_in, + timeAheadNoDot(channelScreenModel.expirationDate, context), + ), fontSize = Font12SP, color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, @@ -207,14 +212,6 @@ fun KeyboardLeadingIcon( onImageChosen = channelScreenModel::pickedMedia, ) - SelectFromFiles( - isUploading = channelScreenModel.isUploadingImage, - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier, - ) { - channelScreenModel.pickedMedia(it) - } - ToggleNip17Button(channelScreenModel, accountViewModel) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 9014fb1ac7..98b36ef0eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -50,6 +51,7 @@ import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.SplitBuilder @@ -85,6 +87,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.notify import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup @@ -92,6 +95,7 @@ import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount import com.vitorpamplona.quartz.nip92IMeta.imetas import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow @@ -101,7 +105,8 @@ import kotlinx.coroutines.launch @Stable open class ChannelNewMessageViewModel : ViewModel(), - ILocationGrabber { + ILocationGrabber, + IExpiration { val draftTag = DraftTagState() init { @@ -149,6 +154,10 @@ open class ChannelNewMessageViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -229,6 +238,10 @@ open class ChannelNewMessageViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null @@ -388,6 +401,7 @@ open class ChannelNewMessageViewModel : val geoHash = if (wantsToAddGeoHash) (location?.value as? LocationState.LocationResult.Success)?.geoHash?.toString() else null val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null return if (channel is PublicChatChannel) { val replyingToEvent = replyTo.value?.toEventHint() @@ -401,6 +415,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } geoHash?.let { geohash(it) } @@ -414,6 +429,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } geoHash?.let { geohash(it) } @@ -426,6 +442,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } geoHash?.let { geohash(it) } @@ -445,6 +462,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -457,6 +475,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -467,6 +486,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -482,6 +502,7 @@ open class ChannelNewMessageViewModel : references(findURLs(tagger.message)) quotes(findNostrUris(tagger.message)) contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -663,4 +684,12 @@ open class ChannelNewMessageViewModel : wantsToMarkAsSensitive = !wantsToMarkAsSensitive draftTag.newVersion() } + + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 5f781ace39..aa651348ec 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensiti import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton @@ -230,6 +232,15 @@ private fun NewProductBody( } } + if (postViewModel.wantsExpirationDate) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + ExpirationDatePicker(postViewModel) + } + } + if (postViewModel.wantsToAddGeoHash) { Row( verticalAlignment = CenterVertically, @@ -388,6 +399,10 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) { postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index fc95787d25..be86ac1511 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -79,6 +81,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiser import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -89,6 +92,7 @@ import com.vitorpamplona.quartz.nip99Classifieds.image import com.vitorpamplona.quartz.nip99Classifieds.tags.ConditionTag import com.vitorpamplona.quartz.nip99Classifieds.tags.PriceTag import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -102,7 +106,8 @@ open class NewProductViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() lateinit var accountViewModel: AccountViewModel @@ -160,6 +165,10 @@ open class NewProductViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -254,6 +263,10 @@ open class NewProductViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null @@ -333,6 +346,7 @@ open class NewProductViewModel : val zapReceiver = if (wantsForwardZapTo) forwardZapTo.value.toZapSplitSetup() else null val localZapRaiserAmount = if (wantsZapraiser) zapRaiserAmount.value else null val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null val quotes = findNostrUris(tagger.message) @@ -353,6 +367,7 @@ open class NewProductViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -618,6 +633,14 @@ open class NewProductViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + fun updateTitle(it: TextFieldValue) { title = it draftTag.newVersion() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index ec7447f9b8..422394b5bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -56,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationControlle import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -144,7 +145,8 @@ open class ShortNotePostViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() lateinit var accountViewModel: AccountViewModel @@ -242,7 +244,7 @@ open class ShortNotePostViewModel : // Expiration Date (NIP-40) var wantsExpirationDate by mutableStateOf(false) - var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) // GeoHash var wantsToAddGeoHash by mutableStateOf(false) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 50988c0654..a442d467cd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -70,6 +70,8 @@ import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.ContentSensiti import com.vitorpamplona.amethyst.ui.note.creators.contentWarning.MarkAsSensitiveButton import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.ShowEmojiSuggestionList import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDateButton +import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.AddLnInvoiceButton import com.vitorpamplona.amethyst.ui.note.creators.invoice.NewPostInvoiceRequest import com.vitorpamplona.amethyst.ui.note.creators.location.AddGeoHashButton @@ -218,6 +220,15 @@ fun PublicMessageScreenContent( ) } + if (postViewModel.wantsExpirationDate) { + Row( + verticalAlignment = CenterVertically, + modifier = Modifier.padding(vertical = Size5dp, horizontal = Size10dp), + ) { + ExpirationDatePicker(postViewModel) + } + } + if (postViewModel.wantsToAddGeoHash) { LocationAsHash(postViewModel) } @@ -350,6 +361,10 @@ private fun BottomRowActions( postViewModel.toggleMarkAsSensitive() } + ExpirationDateButton(postViewModel.wantsExpirationDate) { + postViewModel.toggleExpirationDate() + } + AddGeoHashButton(postViewModel.wantsToAddGeoHash) { postViewModel.wantsToAddGeoHash = !postViewModel.wantsToAddGeoHash } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 7824f39746..d3ccd54210 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessag import android.content.Context import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue @@ -48,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState +import com.vitorpamplona.amethyst.ui.note.creators.expiration.IExpiration import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber import com.vitorpamplona.amethyst.ui.note.creators.messagefield.IMessageField import com.vitorpamplona.amethyst.ui.note.creators.previews.PreviewState @@ -86,6 +88,7 @@ import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarning import com.vitorpamplona.quartz.nip36SensitiveContent.contentWarningReason import com.vitorpamplona.quartz.nip36SensitiveContent.isSensitive import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent +import com.vitorpamplona.quartz.nip40Expiration.expiration import com.vitorpamplona.quartz.nip57Zaps.splits.ZapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplitSetup import com.vitorpamplona.quartz.nip57Zaps.splits.zapSplits @@ -104,6 +107,7 @@ import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent import com.vitorpamplona.quartz.nip94FileMetadata.size import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.TimeUtils import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow @@ -116,7 +120,8 @@ class NewPublicMessageViewModel : ILocationGrabber, IMessageField, IZapField, - IZapRaiser { + IZapRaiser, + IExpiration { val draftTag = DraftTagState() lateinit var accountViewModel: AccountViewModel @@ -171,6 +176,10 @@ class NewPublicMessageViewModel : var wantsToMarkAsSensitive by mutableStateOf(false) var contentWarningDescription by mutableStateOf("") + // Expiration Date (NIP-40) + var wantsExpirationDate by mutableStateOf(false) + override var expirationDate by mutableLongStateOf(TimeUtils.oneDayAhead()) + // GeoHash var wantsToAddGeoHash by mutableStateOf(false) var location: StateFlow? = null @@ -270,6 +279,10 @@ class NewPublicMessageViewModel : wantsToMarkAsSensitive = draftEvent.isSensitive() contentWarningDescription = draftEvent.contentWarningReason() ?: "" + val draftExpiration = draftEvent.expiration() + wantsExpirationDate = draftExpiration != null + expirationDate = draftExpiration ?: TimeUtils.oneDayAhead() + val geohash = draftEvent.getGeoHash() wantsToAddGeoHash = geohash != null @@ -368,6 +381,7 @@ class NewPublicMessageViewModel : val usedAttachments = iMetaAttachments.filterIsIn(urls.toSet()) val contentWarningReason = if (wantsToMarkAsSensitive) contentWarningDescription else null + val localExpirationDate = if (wantsExpirationDate) expirationDate else null return PublicMessageEvent.build( to = toUsers.toList(), @@ -381,6 +395,7 @@ class NewPublicMessageViewModel : localZapRaiserAmount?.let { zapraiser(it) } zapReceiver?.let { zapSplits(it) } contentWarningReason?.let { contentWarning(it) } + localExpirationDate?.let { expiration(it) } emojis(emojis) imetas(usedAttachments) @@ -663,5 +678,13 @@ class NewPublicMessageViewModel : draftTag.newVersion() } + fun toggleExpirationDate() { + wantsExpirationDate = !wantsExpirationDate + if (wantsExpirationDate) { + expirationDate = TimeUtils.oneDayAhead() + } + draftTag.newVersion() + } + override fun locationManager(): LocationState = Amethyst.instance.locationManager } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index 62e5657f88..27bc8972ad 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -91,6 +91,7 @@ import com.vitorpamplona.amethyst.ui.note.CheckAndDisplayEditStatus import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.ui.note.DisplayDraft import com.vitorpamplona.amethyst.ui.note.DisplayOtsIfInOriginal +import com.vitorpamplona.amethyst.ui.note.Expiration import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture @@ -483,6 +484,8 @@ private fun FullBleedNoteCompose( CheckAndDisplayEditStatus(editState) + Expiration(baseNote) + TimeAgo(note = baseNote) MoreOptionsButton(baseNote, editState, accountViewModel, nav) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6aec4ff455..b6e772b0dd 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1538,6 +1538,7 @@ Crash Report found Would you like to send the recent crash report to Amethyst in a DM? No personal information will be shared Send it + This message will disappear in %1$s This message will disappear in %1$d days Select Signer From 1468a41737faf2b9ddb7c950eb27ea737fa01278 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 18:07:33 -0400 Subject: [PATCH 064/172] Adds expirations to change log --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4ac7c405..193605cf98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,18 +23,20 @@ Adds support for NIP-39 External Identities with kind 10011 Adds support for NIP-C0 Code Snippets -Adds support for NIP-66 Relay Monitor and discovery support to Quartz - Adds support for NIP-A3 Payment targets (PayTo: 10133) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 Adds support for BUD-10 "Blossom:" URIs in images, audios, videos, and documents. +Adds support for NIP-40 Expirations in any new post. + +Adds support for NIP-66 Relay Monitor and discovery support to Quartz + Adds support for Namecoin .bit urls to NIP-05 - Adds choice of ElectrumX server to resolve namecoins. Adds basic support for Chess with Jester protocol -Adds NIP-46 support to Quartz and Amethyst Desktop +Adds NIP-46 Bunker support to Quartz and Amethyst Desktop Adds a Broadcasting feedback pop-up in the Complete UI mode From bb882280190d1eeeabbd6bf2dc12878cb34fa9e2 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 13 Mar 2026 22:10:50 +0000 Subject: [PATCH 065/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 2efa4044aa..a10363d70b 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -1094,6 +1094,13 @@ Anuluj podział Zap Dodaj ostrzeżenie o treści Usuń ostrzeżenie o treści + Dodaj datę wygaśnięcia + Usuń datę wygaśnięcia + Data wygaśnięcia + Post zostanie ukryty przez klientów po tej dacie (NIP-40) + Wybierz datę i godzinę wygaśnięcia + Wygasa za %1$s + Czas wygaśnięcia Pokaż npub jako QR kod Pokaż nprofile jako kod QR Nieprawidłowy adres @@ -1163,6 +1170,9 @@ Zablokowane Transmitery Zablokowane Transmitery Amethyst nigdy nie połączy się z tymi transmiterami + Eksportuj ustawienia transmiterów + Eksportuj jako tekst + Eksportuj jako ZIP (JSON) Wspieraj deweloperów! Twoja darowizna pomaga nam coś zmienić. Każdy sat się liczy! Przekaż darowiznę @@ -1268,6 +1278,18 @@ Szukaj tagu: #%1$s Nie tłumacz z Języki wyświetlane tutaj nie będą tłumaczone. Wybierz język, aby usunąć go z listy języków nietłumaczonych. + Przetłumacz na + Wybierz język, na który chcesz przetłumaczyć treść. + Ustawienia językowe + Dla każdej pary językowej wybierz, który język ma być wyświetlany jako pierwszy. + Wyszukiwanie Języków + Dodaj język + Dodaj pary językowe + Język źródłowy + Język docelowy + Najpierw pokaż %1$s + Nie ma jeszcze żadnych ustawień językowych. Są one tworzone automatycznie w miarę pojawiania się tłumaczeń, ale można je też dodać ręcznie. + Usuń ustawienia Pauza Odtwórz Otwórz rozwijane menu From b3adc56adf95319e7fc433d3095c02ecd349e278 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 22:11:54 +0000 Subject: [PATCH 066/172] feat: replace dialog with swipe-to-reveal delete button for drafts Instead of showing a confirmation dialog, the swipe now stays open revealing a red background with a clickable "Request Deletion" button. Supports swiping from both left-to-right and right-to-left directions. The user taps the revealed button to confirm deletion. https://claude.ai/code/session_01JWJxejv2EKrPj2KGEfPPxU --- .../amethyst/ui/components/SwipeToDelete.kt | 106 ++++++++++-------- .../screen/loggedIn/drafts/DraftListScreen.kt | 2 +- amethyst/src/main/res/values/strings.xml | 1 - 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt index e64d2534bc..b2748732e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.components import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope @@ -30,7 +31,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -40,14 +40,11 @@ import androidx.compose.material3.SwipeToDismissBoxValue.EndToStart import androidx.compose.material3.SwipeToDismissBoxValue.Settled import androidx.compose.material3.SwipeToDismissBoxValue.StartToEnd import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -56,6 +53,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -98,56 +96,26 @@ fun SwipeToDeleteContainer( @Composable fun SwipeToDeleteWithConfirmation( modifier: Modifier = Modifier, - onStartToEnd: () -> Unit, + onDelete: () -> Unit, content: @Composable (RowScope.() -> Unit), ) { - var showConfirmDialog by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() val dismissState = rememberSwipeToDismissBoxState( - confirmValueChange = { - when (it) { - StartToEnd -> { - showConfirmDialog = true - } - EndToStart -> {} - Settled -> {} - } - return@rememberSwipeToDismissBoxState false - }, positionalThreshold = { it * .40f }, ) - if (showConfirmDialog) { - AlertDialog( - onDismissRequest = { showConfirmDialog = false }, - title = { Text(text = stringRes(id = R.string.request_deletion)) }, - text = { Text(text = stringRes(id = R.string.delete_draft_confirmation)) }, - confirmButton = { - TextButton( - onClick = { - showConfirmDialog = false - onStartToEnd() - }, - ) { - Text(text = stringRes(id = R.string.yes)) - } - }, - dismissButton = { - TextButton( - onClick = { showConfirmDialog = false }, - ) { - Text(text = stringRes(id = R.string.no)) - } - }, - ) - } - SwipeToDismissBox( state = dismissState, modifier = modifier, - backgroundContent = { DismissBackground(dismissState) }, - enableDismissFromEndToStart = false, + backgroundContent = { + ConfirmDeleteBackground(dismissState) { + onDelete() + scope.launch { dismissState.reset() } + } + }, + enableDismissFromEndToStart = true, content = content, ) } @@ -191,3 +159,53 @@ fun DismissBackground(dismissState: SwipeToDismissBoxState) { ) } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfirmDeleteBackground( + dismissState: SwipeToDismissBoxState, + onConfirmDelete: () -> Unit, +) { + val settled = dismissState.currentValue == Settled && dismissState.targetValue == Settled + + val color by animateColorAsState( + if (!settled) { + Color(0xFFFF1744) + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + label = "ConfirmDeleteBackground", + ) + + val haptic = LocalHapticFeedback.current + LaunchedEffect(key1 = dismissState.currentValue > dismissState.targetValue) { + if (dismissState.progress > 0 && dismissState.progress < 1) { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + } + + Row( + modifier = + Modifier + .fillMaxSize() + .background(color) + .clickable(enabled = !settled) { onConfirmDelete() } + .padding(20.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Icon( + Icons.Default.Delete, + contentDescription = stringRes(id = R.string.request_deletion), + ) + Text( + text = stringRes(id = R.string.request_deletion), + color = Color.White, + style = MaterialTheme.typography.titleMedium, + ) + Icon( + Icons.Default.Delete, + contentDescription = stringRes(id = R.string.request_deletion), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt index fcc2f3eb6e..716b4545a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/drafts/DraftListScreen.kt @@ -170,7 +170,7 @@ private fun DraftFeedLoaded( Row(Modifier.fillMaxWidth().animateItem()) { SwipeToDeleteWithConfirmation( modifier = Modifier.fillMaxWidth().animateContentSize(), - onStartToEnd = { accountViewModel.delete(item) }, + onDelete = { accountViewModel.delete(item) }, ) { NoteCompose( item, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 8a710fd466..72f3724dc7 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1473,7 +1473,6 @@ Add a Blossom Server Delete all Are you sure you want to delete all drafts? - Are you sure you want to delete this draft? " +%1$s" Stack: From 474a875167533f514a35633ce62b0b6a479b102e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 13 Mar 2026 19:11:10 -0400 Subject: [PATCH 067/172] Fixes padding of the expiration date on NoteCompose --- .../java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 42ab09d74f..5a2163075d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -30,6 +30,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Timer @@ -1358,7 +1359,7 @@ fun DisplayExpiration(expirationDate: Long) { Icon( imageVector = Icons.Outlined.Timer, contentDescription = stringRes(R.string.expiration_date_label), - modifier = Modifier.size(15.dp), + modifier = Modifier.padding(start = 5.dp).size(15.dp), tint = MaterialTheme.colorScheme.placeholderText, ) val context = LocalContext.current @@ -1366,6 +1367,7 @@ fun DisplayExpiration(expirationDate: Long) { text = timeAheadNoDot(expirationDate, context), color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, + modifier = Modifier.padding(start = 3.dp), ) } } From bcb5be76c7a967bb9c05681469a98b4c5233bc1b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Mar 2026 23:19:47 +0000 Subject: [PATCH 068/172] feat: Add cancel button to swipe-to-delete confirmation After swiping a draft to delete, the red confirmation background now shows both a "Delete" button (left) and a "Cancel" button (right). Tapping Cancel resets the swipe state, allowing users to dismiss the delete action without deleting the draft. https://claude.ai/code/session_01ULVR1fKwka5TgfgSucPE4r --- .../amethyst/ui/components/SwipeToDelete.kt | 73 ++++++++++++++----- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt index b2748732e2..68c3d425dd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SwipeToDelete.kt @@ -27,9 +27,11 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -110,10 +112,16 @@ fun SwipeToDeleteWithConfirmation( state = dismissState, modifier = modifier, backgroundContent = { - ConfirmDeleteBackground(dismissState) { - onDelete() - scope.launch { dismissState.reset() } - } + ConfirmDeleteBackground( + dismissState = dismissState, + onConfirmDelete = { + onDelete() + scope.launch { dismissState.reset() } + }, + onCancel = { + scope.launch { dismissState.reset() } + }, + ) }, enableDismissFromEndToStart = true, content = content, @@ -165,6 +173,7 @@ fun DismissBackground(dismissState: SwipeToDismissBoxState) { fun ConfirmDeleteBackground( dismissState: SwipeToDismissBoxState, onConfirmDelete: () -> Unit, + onCancel: () -> Unit, ) { val settled = dismissState.currentValue == Settled && dismissState.targetValue == Settled @@ -189,23 +198,51 @@ fun ConfirmDeleteBackground( Modifier .fillMaxSize() .background(color) - .clickable(enabled = !settled) { onConfirmDelete() } .padding(20.dp, 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - Icons.Default.Delete, - contentDescription = stringRes(id = R.string.request_deletion), - ) - Text( - text = stringRes(id = R.string.request_deletion), - color = Color.White, - style = MaterialTheme.typography.titleMedium, - ) - Icon( - Icons.Default.Delete, - contentDescription = stringRes(id = R.string.request_deletion), - ) + Row( + modifier = + Modifier + .weight(1f) + .fillMaxHeight() + .clickable(enabled = !settled) { onConfirmDelete() }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Icon( + Icons.Default.Delete, + contentDescription = stringRes(id = R.string.request_deletion), + tint = Color.White, + ) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text( + text = stringRes(id = R.string.request_deletion), + color = Color.White, + style = MaterialTheme.typography.titleMedium, + ) + } + Row( + modifier = + Modifier + .weight(1f) + .fillMaxHeight() + .clickable(enabled = !settled) { onCancel() }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Icon( + Icons.Default.Close, + contentDescription = stringRes(id = R.string.cancel), + tint = Color.White, + ) + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + Text( + text = stringRes(id = R.string.cancel), + color = Color.White, + style = MaterialTheme.typography.titleMedium, + ) + } } } From 9e4cb446efdc06d29ff6ec7a3762628c2816ba4f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 02:58:40 +0000 Subject: [PATCH 069/172] feat: add NIP-47 get_budget, sign_message, create_connection methods and Alby Hub interop - Add get_budget, sign_message, create_connection request/response types - Add NwcTransactionType (incoming/outgoing) and NwcTransactionState (PENDING/SETTLED/FAILED/ACCEPTED) constants - Add missing error codes: BAD_REQUEST, NOT_FOUND, EXPIRED - Add settle_deadline field to NwcTransaction - Add total_count to ListTransactionsResult - Add metadata and lud16 to GetInfoResult - Add unpaid_outgoing and unpaid_incoming to ListTransactionsParams - Update Jackson deserializers for new method types - Add AlbyInteropTest with 25+ tests verifying compatibility with Alby Hub's JSON formats for all request/response types - Update existing tests for new constants and error codes https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../quartz/nip47WalletConnect/NwcErrorCode.kt | 3 + .../quartz/nip47WalletConnect/NwcMethod.kt | 3 + .../nip47WalletConnect/NwcTransaction.kt | 13 + .../quartz/nip47WalletConnect/Request.kt | 56 +++ .../quartz/nip47WalletConnect/Response.kt | 34 ++ .../nip47WalletConnect/AlbyInteropTest.kt | 325 ++++++++++++++++++ .../nip47WalletConnect/NwcMethodTest.kt | 22 +- .../quartz/nip47WalletConnect/RequestTest.kt | 91 +++++ .../quartz/nip47WalletConnect/ResponseTest.kt | 111 ++++++ .../jackson/RequestDeserializer.kt | 6 + .../jackson/ResponseDeserializer.kt | 6 + 11 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcErrorCode.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcErrorCode.kt index f88fd18924..cef350602a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcErrorCode.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcErrorCode.kt @@ -30,6 +30,9 @@ enum class NwcErrorCode { UNAUTHORIZED, INTERNAL, UNSUPPORTED_ENCRYPTION, + BAD_REQUEST, + NOT_FOUND, + EXPIRED, OTHER, } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethod.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethod.kt index e6195639a8..afc6474ba4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethod.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethod.kt @@ -28,6 +28,9 @@ object NwcMethod { const val LIST_TRANSACTIONS = "list_transactions" const val GET_BALANCE = "get_balance" const val GET_INFO = "get_info" + const val GET_BUDGET = "get_budget" + const val SIGN_MESSAGE = "sign_message" + const val CREATE_CONNECTION = "create_connection" const val MAKE_HOLD_INVOICE = "make_hold_invoice" const val CANCEL_HOLD_INVOICE = "cancel_hold_invoice" const val SETTLE_HOLD_INVOICE = "settle_hold_invoice" diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt index d2a043f59c..9ba84991ef 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt @@ -20,6 +20,18 @@ */ package com.vitorpamplona.quartz.nip47WalletConnect +object NwcTransactionType { + const val INCOMING = "incoming" + const val OUTGOING = "outgoing" +} + +object NwcTransactionState { + const val PENDING = "PENDING" + const val SETTLED = "SETTLED" + const val FAILED = "FAILED" + const val ACCEPTED = "ACCEPTED" +} + class NwcTransaction( var type: String? = null, var state: String? = null, @@ -33,6 +45,7 @@ class NwcTransaction( var created_at: Long? = null, var expires_at: Long? = null, var settled_at: Long? = null, + var settle_deadline: Long? = null, var metadata: Any? = null, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt index 8d0ea548b2..cdce4c7ea3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt @@ -113,6 +113,8 @@ class ListTransactionsParams( var limit: Int? = null, var offset: Int? = null, var unpaid: Boolean? = null, + var unpaid_outgoing: Boolean? = null, + var unpaid_incoming: Boolean? = null, var type: String? = null, ) @@ -198,3 +200,57 @@ class SettleHoldInvoiceMethod( fun create(preimage: String): SettleHoldInvoiceMethod = SettleHoldInvoiceMethod(SettleHoldInvoiceParams(preimage)) } } + +// get_budget +class GetBudgetMethod : Request(NwcMethod.GET_BUDGET) { + companion object { + fun create(): GetBudgetMethod = GetBudgetMethod() + } +} + +// sign_message +class SignMessageParams( + var message: String? = null, +) + +class SignMessageMethod( + var params: SignMessageParams? = null, +) : Request(NwcMethod.SIGN_MESSAGE) { + companion object { + fun create(message: String): SignMessageMethod = SignMessageMethod(SignMessageParams(message)) + } +} + +// create_connection +class CreateConnectionParams( + var pubkey: String? = null, + var name: String? = null, + var request_methods: List? = null, + var notification_types: List? = null, + var max_amount: Long? = null, + var budget_renewal: String? = null, + var expires_at: Long? = null, + var isolated: Boolean? = null, + var metadata: Any? = null, +) + +class CreateConnectionMethod( + var params: CreateConnectionParams? = null, +) : Request(NwcMethod.CREATE_CONNECTION) { + companion object { + fun create( + pubkey: String, + name: String, + requestMethods: List? = null, + notificationTypes: List? = null, + maxAmount: Long? = null, + budgetRenewal: String? = null, + expiresAt: Long? = null, + isolated: Boolean? = null, + metadata: Any? = null, + ): CreateConnectionMethod = + CreateConnectionMethod( + CreateConnectionParams(pubkey, name, requestMethods, notificationTypes, maxAmount, budgetRenewal, expiresAt, isolated, metadata), + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt index 5c4d3d6a02..623f0cee6a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt @@ -79,6 +79,7 @@ class ListTransactionsSuccessResponse( ) : Response(NwcMethod.LIST_TRANSACTIONS) { class ListTransactionsResult( val transactions: List? = null, + val total_count: Long? = null, ) } @@ -104,6 +105,8 @@ class GetInfoSuccessResponse( val block_hash: String? = null, val methods: List? = null, val notifications: List? = null, + val metadata: Any? = null, + val lud16: String? = null, ) } @@ -121,3 +124,34 @@ class CancelHoldInvoiceSuccessResponse( class SettleHoldInvoiceSuccessResponse( val result: Any? = null, ) : Response(NwcMethod.SETTLE_HOLD_INVOICE) + +// get_budget success response +class GetBudgetSuccessResponse( + val result: GetBudgetResult? = null, +) : Response(NwcMethod.GET_BUDGET) { + class GetBudgetResult( + val used_budget: Long? = null, + val total_budget: Long? = null, + val renews_at: Long? = null, + val renewal_period: String? = null, + ) +} + +// sign_message success response +class SignMessageSuccessResponse( + val result: SignMessageResult? = null, +) : Response(NwcMethod.SIGN_MESSAGE) { + class SignMessageResult( + val message: String? = null, + val signature: String? = null, + ) +} + +// create_connection success response +class CreateConnectionSuccessResponse( + val result: CreateConnectionResult? = null, +) : Response(NwcMethod.CREATE_CONNECTION) { + class CreateConnectionResult( + val wallet_pubkey: String? = null, + ) +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt new file mode 100644 index 0000000000..babb67d670 --- /dev/null +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +/** + * Interoperability tests using JSON structures matching Alby Hub's NIP-47 implementation. + * These tests verify that Amethyst can correctly parse responses from Alby Hub wallets. + */ +class AlbyInteropTest { + // --- Alby Hub pay_invoice response format --- + + @Test + fun testAlbyPayInvoiceSuccess() { + val json = """{"result_type":"pay_invoice","result":{"preimage":"6565656565656565656565656565656565656565656565656565656565656565","fees_paid":1}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("6565656565656565656565656565656565656565656565656565656565656565", response.result?.preimage) + assertEquals(1L, response.result?.fees_paid) + } + + @Test + fun testAlbyPayInvoiceInsufficientBalance() { + val json = """{"result_type":"pay_invoice","error":{"code":"INSUFFICIENT_BALANCE","message":"insufficient funds available to send"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.INSUFFICIENT_BALANCE, response.error?.code) + assertEquals("insufficient funds available to send", response.error?.message) + } + + @Test + fun testAlbyPayInvoiceBadRequest() { + val json = """{"result_type":"pay_invoice","error":{"code":"BAD_REQUEST","message":"Failed to decode bolt11 invoice: bolt11 too short"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.BAD_REQUEST, response.error?.code) + } + + // --- Alby Hub get_balance response format --- + + @Test + fun testAlbyGetBalanceResponse() { + val json = """{"result_type":"get_balance","result":{"balance":21000000}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(21000000L, response.result?.balance) + } + + // --- Alby Hub get_info response format (with extended fields) --- + + @Test + fun testAlbyGetInfoFullResponse() { + val json = + """{"result_type":"get_info","result":{"alias":"AlbyHub","color":"#3399ff","pubkey":"02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc","network":"mainnet","block_height":800000,"block_hash":"00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72f2e4b10","methods":["pay_invoice","pay_keysend","get_balance","get_budget","get_info","make_invoice","lookup_invoice","list_transactions","sign_message"],"notifications":["payment_received","payment_sent"],"lud16":"satoshi@getalby.com"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val result = response.result + assertNotNull(result) + assertEquals("AlbyHub", result.alias) + assertEquals("#3399ff", result.color) + assertEquals("02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc", result.pubkey) + assertEquals("mainnet", result.network) + assertEquals(800000L, result.block_height) + assertEquals("00000000000000000002a7c4c1e48d76c5a37902165a270156b7a8d72f2e4b10", result.block_hash) + assertEquals(9, result.methods?.size) + assertEquals(2, result.notifications?.size) + assertEquals("satoshi@getalby.com", result.lud16) + } + + // --- Alby Hub get_budget response format --- + + @Test + fun testAlbyGetBudgetWithRenewal() { + val json = """{"result_type":"get_budget","result":{"used_budget":50000,"total_budget":100000,"renews_at":1700000000,"renewal_period":"monthly"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val result = response.result + assertNotNull(result) + assertEquals(50000L, result.used_budget) + assertEquals(100000L, result.total_budget) + assertEquals(1700000000L, result.renews_at) + assertEquals("monthly", result.renewal_period) + } + + @Test + fun testAlbyGetBudgetUnlimited() { + val json = """{"result_type":"get_budget","result":{"used_budget":0,"total_budget":0,"renewal_period":"never"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(0L, response.result?.used_budget) + assertEquals(0L, response.result?.total_budget) + assertNull(response.result?.renews_at) + assertEquals("never", response.result?.renewal_period) + } + + // --- Alby Hub make_invoice response format --- + + @Test + fun testAlbyMakeInvoiceResponse() { + val json = + """{"result_type":"make_invoice","result":{"type":"incoming","state":"PENDING","invoice":"lnbc10n1pj3xyz...","description":"Test invoice","payment_hash":"abc123def456","amount":1000,"fees_paid":0,"created_at":1693876497,"expires_at":1694876497}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val txn = response.result + assertNotNull(txn) + assertEquals(NwcTransactionType.INCOMING, txn.type) + assertEquals(NwcTransactionState.PENDING, txn.state) + assertEquals("lnbc10n1pj3xyz...", txn.invoice) + assertEquals("Test invoice", txn.description) + assertEquals("abc123def456", txn.payment_hash) + assertEquals(1000L, txn.amount) + assertEquals(0L, txn.fees_paid) + } + + // --- Alby Hub lookup_invoice with settled state --- + + @Test + fun testAlbyLookupInvoiceSettled() { + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"SETTLED","invoice":"lnbc50n1...","preimage":"preimage123","payment_hash":"hash456","amount":5000,"fees_paid":0,"created_at":1693876497,"settled_at":1694876500}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val txn = response.result + assertNotNull(txn) + assertEquals(NwcTransactionType.INCOMING, txn.type) + assertEquals(NwcTransactionState.SETTLED, txn.state) + assertEquals("preimage123", txn.preimage) + assertEquals(1694876500L, txn.settled_at) + } + + @Test + fun testAlbyLookupInvoiceNotFound() { + val json = """{"result_type":"lookup_invoice","error":{"code":"NOT_FOUND","message":"transaction not found"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.NOT_FOUND, response.error?.code) + } + + // --- Alby Hub list_transactions with total_count --- + + @Test + fun testAlbyListTransactionsWithTotalCount() { + val json = + """{"result_type":"list_transactions","result":{"transactions":[{"type":"incoming","state":"SETTLED","invoice":"lnbc1...","payment_hash":"h1","amount":1000,"fees_paid":0,"created_at":1693876497,"settled_at":1694876500},{"type":"outgoing","state":"SETTLED","invoice":"lnbc2...","payment_hash":"h2","amount":2000,"fees_paid":10,"created_at":1693876400,"settled_at":1694876400}],"total_count":100}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(2, response.result?.transactions?.size) + assertEquals(100L, response.result?.total_count) + + val first = response.result?.transactions?.get(0) + assertEquals(NwcTransactionType.INCOMING, first?.type) + assertEquals(NwcTransactionState.SETTLED, first?.state) + + val second = response.result?.transactions?.get(1) + assertEquals(NwcTransactionType.OUTGOING, second?.type) + assertEquals(10L, second?.fees_paid) + } + + // --- Alby Hub sign_message response --- + + @Test + fun testAlbySignMessageResponse() { + val json = """{"result_type":"sign_message","result":{"message":"Hello Nostr","signature":"3045022100..."}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("Hello Nostr", response.result?.message) + assertEquals("3045022100...", response.result?.signature) + } + + // --- Alby Hub create_connection response --- + + @Test + fun testAlbyCreateConnectionResponse() { + val json = """{"result_type":"create_connection","result":{"wallet_pubkey":"02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc", response.result?.wallet_pubkey) + } + + // --- Alby Hub notification formats --- + + @Test + fun testAlbyPaymentReceivedNotification() { + val json = + """{"notification_type":"payment_received","notification":{"type":"incoming","state":"SETTLED","invoice":"lnbc50n1...","preimage":"pre123","payment_hash":"hash123","amount":5000,"fees_paid":0,"created_at":1693876497,"settled_at":1694876500}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertIs(notification) + val txn = notification.notification + assertNotNull(txn) + assertEquals(NwcTransactionType.INCOMING, txn.type) + assertEquals(NwcTransactionState.SETTLED, txn.state) + assertEquals(5000L, txn.amount) + } + + @Test + fun testAlbyPaymentSentNotification() { + val json = + """{"notification_type":"payment_sent","notification":{"type":"outgoing","state":"SETTLED","invoice":"lnbc100n1...","preimage":"pre456","payment_hash":"hash456","amount":10000,"fees_paid":10,"created_at":1693876497,"settled_at":1694876500}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertIs(notification) + val txn = notification.notification + assertNotNull(txn) + assertEquals(NwcTransactionType.OUTGOING, txn.type) + assertEquals(NwcTransactionState.SETTLED, txn.state) + assertEquals(10000L, txn.amount) + assertEquals(10L, txn.fees_paid) + } + + @Test + fun testAlbyHoldInvoiceAcceptedNotification() { + val json = + """{"notification_type":"hold_invoice_accepted","notification":{"type":"incoming","invoice":"lnbc200n1...","payment_hash":"hash789","amount":20000,"created_at":1693876497,"expires_at":1694876497,"settle_deadline":800000}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertIs(notification) + val data = notification.notification + assertNotNull(data) + assertEquals("incoming", data.type) + assertEquals(20000L, data.amount) + assertEquals(800000L, data.settle_deadline) + } + + // --- Alby Hub error code interop --- + + @Test + fun testAlbyRestricted() { + val json = """{"result_type":"pay_invoice","error":{"code":"RESTRICTED","message":"This app does not have the pay_invoice scope"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.RESTRICTED, response.error?.code) + } + + @Test + fun testAlbyExpiredConnection() { + val json = """{"result_type":"get_balance","error":{"code":"EXPIRED","message":"This app has expired"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.EXPIRED, response.error?.code) + } + + @Test + fun testAlbyQuotaExceeded() { + val json = """{"result_type":"pay_invoice","error":{"code":"QUOTA_EXCEEDED","message":"Exceeded budget limit"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.QUOTA_EXCEEDED, response.error?.code) + } + + // --- Alby Hub request format interop --- + + @Test + fun testAlbyPayInvoiceRequestFormat() { + val json = """{"method":"pay_invoice","params":{"invoice":"lnbc10u1pj3xyz..."}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("lnbc10u1pj3xyz...", request.params?.invoice) + } + + @Test + fun testAlbyGetBudgetRequestFormat() { + val json = """{"method":"get_budget","params":{}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + } + + @Test + fun testAlbySignMessageRequestFormat() { + val json = """{"method":"sign_message","params":{"message":"Hello from Amethyst"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("Hello from Amethyst", request.params?.message) + } + + @Test + fun testAlbyCreateConnectionRequestFormat() { + val json = + """{"method":"create_connection","params":{"pubkey":"02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc","name":"Amethyst","request_methods":["pay_invoice","get_balance","get_info","make_invoice","lookup_invoice","list_transactions"],"notification_types":["payment_received","payment_sent"],"max_amount":100000,"budget_renewal":"monthly","isolated":false}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc", request.params?.pubkey) + assertEquals("Amethyst", request.params?.name) + assertEquals(6, request.params?.request_methods?.size) + assertEquals(2, request.params?.notification_types?.size) + assertEquals(100000L, request.params?.max_amount) + assertEquals("monthly", request.params?.budget_renewal) + assertEquals(false, request.params?.isolated) + } + + // --- Transaction with settle_deadline from Alby hold invoice --- + + @Test + fun testAlbyMakeHoldInvoiceWithSettleDeadline() { + val json = + """{"result_type":"make_hold_invoice","result":{"type":"incoming","state":"PENDING","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"expires_at":2000,"settle_deadline":144}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val txn = response.result + assertNotNull(txn) + assertEquals(144L, txn.settle_deadline) + assertEquals(NwcTransactionState.PENDING, txn.state) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt index f088a2dbd5..4d76bb9261 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt @@ -33,6 +33,9 @@ class NwcMethodTest { assertEquals("list_transactions", NwcMethod.LIST_TRANSACTIONS) assertEquals("get_balance", NwcMethod.GET_BALANCE) assertEquals("get_info", NwcMethod.GET_INFO) + assertEquals("get_budget", NwcMethod.GET_BUDGET) + assertEquals("sign_message", NwcMethod.SIGN_MESSAGE) + assertEquals("create_connection", NwcMethod.CREATE_CONNECTION) assertEquals("make_hold_invoice", NwcMethod.MAKE_HOLD_INVOICE) assertEquals("cancel_hold_invoice", NwcMethod.CANCEL_HOLD_INVOICE) assertEquals("settle_hold_invoice", NwcMethod.SETTLE_HOLD_INVOICE) @@ -48,7 +51,7 @@ class NwcMethodTest { @Test fun testErrorCodeValues() { val codes = NwcErrorCode.entries - assertEquals(10, codes.size) + assertEquals(13, codes.size) assertEquals(NwcErrorCode.RATE_LIMITED, NwcErrorCode.valueOf("RATE_LIMITED")) assertEquals(NwcErrorCode.NOT_IMPLEMENTED, NwcErrorCode.valueOf("NOT_IMPLEMENTED")) assertEquals(NwcErrorCode.INSUFFICIENT_BALANCE, NwcErrorCode.valueOf("INSUFFICIENT_BALANCE")) @@ -58,9 +61,26 @@ class NwcMethodTest { assertEquals(NwcErrorCode.UNAUTHORIZED, NwcErrorCode.valueOf("UNAUTHORIZED")) assertEquals(NwcErrorCode.INTERNAL, NwcErrorCode.valueOf("INTERNAL")) assertEquals(NwcErrorCode.UNSUPPORTED_ENCRYPTION, NwcErrorCode.valueOf("UNSUPPORTED_ENCRYPTION")) + assertEquals(NwcErrorCode.BAD_REQUEST, NwcErrorCode.valueOf("BAD_REQUEST")) + assertEquals(NwcErrorCode.NOT_FOUND, NwcErrorCode.valueOf("NOT_FOUND")) + assertEquals(NwcErrorCode.EXPIRED, NwcErrorCode.valueOf("EXPIRED")) assertEquals(NwcErrorCode.OTHER, NwcErrorCode.valueOf("OTHER")) } + @Test + fun testTransactionTypeConstants() { + assertEquals("incoming", NwcTransactionType.INCOMING) + assertEquals("outgoing", NwcTransactionType.OUTGOING) + } + + @Test + fun testTransactionStateConstants() { + assertEquals("PENDING", NwcTransactionState.PENDING) + assertEquals("SETTLED", NwcTransactionState.SETTLED) + assertEquals("FAILED", NwcTransactionState.FAILED) + assertEquals("ACCEPTED", NwcTransactionState.ACCEPTED) + } + @Test fun testNwcError() { val error = NwcError(NwcErrorCode.UNAUTHORIZED, "not allowed") diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt index 630f7addf3..3299b09275 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt @@ -288,6 +288,97 @@ class RequestTest { assertEquals("preimage_xyz", request.params?.preimage) } + // --- GetBudget --- + + @Test + fun testGetBudgetCreate() { + val request = GetBudgetMethod.create() + assertEquals(NwcMethod.GET_BUDGET, request.method) + } + + @Test + fun testGetBudgetSerialization() { + val request = GetBudgetMethod.create() + val json = OptimizedJsonMapper.toJson(request) + assertTrue(json.contains("\"method\":\"get_budget\"")) + } + + @Test + fun testGetBudgetDeserialization() { + val json = """{"method":"get_budget"}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + } + + // --- SignMessage --- + + @Test + fun testSignMessageCreate() { + val request = SignMessageMethod.create("Hello Nostr") + assertEquals(NwcMethod.SIGN_MESSAGE, request.method) + assertEquals("Hello Nostr", request.params?.message) + } + + @Test + fun testSignMessageSerialization() { + val request = SignMessageMethod.create("test message") + val json = OptimizedJsonMapper.toJson(request) + assertTrue(json.contains("\"method\":\"sign_message\"")) + assertTrue(json.contains("\"message\":\"test message\"")) + } + + @Test + fun testSignMessageDeserialization() { + val json = """{"method":"sign_message","params":{"message":"Hello Nostr"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("Hello Nostr", request.params?.message) + } + + // --- CreateConnection --- + + @Test + fun testCreateConnectionCreate() { + val request = + CreateConnectionMethod.create( + pubkey = "abc123", + name = "My App", + requestMethods = listOf("pay_invoice", "get_balance"), + notificationTypes = listOf("payment_received"), + maxAmount = 100000L, + budgetRenewal = "monthly", + ) + assertEquals(NwcMethod.CREATE_CONNECTION, request.method) + assertEquals("abc123", request.params?.pubkey) + assertEquals("My App", request.params?.name) + assertEquals(listOf("pay_invoice", "get_balance"), request.params?.request_methods) + assertEquals(listOf("payment_received"), request.params?.notification_types) + assertEquals(100000L, request.params?.max_amount) + assertEquals("monthly", request.params?.budget_renewal) + } + + @Test + fun testCreateConnectionSerialization() { + val request = CreateConnectionMethod.create(pubkey = "abc123", name = "My App") + val json = OptimizedJsonMapper.toJson(request) + assertTrue(json.contains("\"method\":\"create_connection\"")) + assertTrue(json.contains("\"pubkey\":\"abc123\"")) + assertTrue(json.contains("\"name\":\"My App\"")) + } + + @Test + fun testCreateConnectionDeserialization() { + val json = + """{"method":"create_connection","params":{"pubkey":"abc123","name":"Test App","request_methods":["pay_invoice"],"max_amount":50000,"budget_renewal":"monthly"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("abc123", request.params?.pubkey) + assertEquals("Test App", request.params?.name) + assertEquals(listOf("pay_invoice"), request.params?.request_methods) + assertEquals(50000L, request.params?.max_amount) + assertEquals("monthly", request.params?.budget_renewal) + } + // --- Unknown method --- @Test diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt index 6bf160d6a4..1484833605 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt @@ -255,6 +255,117 @@ class ResponseTest { assertIs(response) } + // --- GetBudget Success --- + + @Test + fun testGetBudgetSuccessDeserialization() { + val json = """{"result_type":"get_budget","result":{"used_budget":50000,"total_budget":100000,"renews_at":1700000000,"renewal_period":"monthly"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result) + assertEquals(50000L, response.result?.used_budget) + assertEquals(100000L, response.result?.total_budget) + assertEquals(1700000000L, response.result?.renews_at) + assertEquals("monthly", response.result?.renewal_period) + } + + @Test + fun testGetBudgetNoBudgetLimit() { + val json = """{"result_type":"get_budget","result":{"used_budget":0,"total_budget":0,"renewal_period":"never"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(0L, response.result?.used_budget) + assertEquals(0L, response.result?.total_budget) + assertNull(response.result?.renews_at) + assertEquals("never", response.result?.renewal_period) + } + + // --- SignMessage Success --- + + @Test + fun testSignMessageSuccessDeserialization() { + val json = """{"result_type":"sign_message","result":{"message":"Hello Nostr","signature":"sig123abc"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result) + assertEquals("Hello Nostr", response.result?.message) + assertEquals("sig123abc", response.result?.signature) + } + + // --- CreateConnection Success --- + + @Test + fun testCreateConnectionSuccessDeserialization() { + val json = """{"result_type":"create_connection","result":{"wallet_pubkey":"walletpub123"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result) + assertEquals("walletpub123", response.result?.wallet_pubkey) + } + + // --- GetInfo with extended fields --- + + @Test + fun testGetInfoWithMetadataAndLud16() { + val json = + """{"result_type":"get_info","result":{"alias":"AlbyHub","methods":["pay_invoice","get_balance"],"notifications":["payment_received"],"lud16":"user@getalby.com"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("AlbyHub", response.result?.alias) + assertEquals("user@getalby.com", response.result?.lud16) + assertEquals(listOf("pay_invoice", "get_balance"), response.result?.methods) + } + + // --- ListTransactions with total_count --- + + @Test + fun testListTransactionsWithTotalCount() { + val json = + """{"result_type":"list_transactions","result":{"transactions":[{"type":"incoming","amount":100,"created_at":1000}],"total_count":42}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(1, response.result?.transactions?.size) + assertEquals(42L, response.result?.total_count) + } + + // --- Transaction with settle_deadline --- + + @Test + fun testTransactionWithSettleDeadline() { + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"ACCEPTED","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"settle_deadline":800000}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(800000L, response.result?.settle_deadline) + assertEquals("ACCEPTED", response.result?.state) + } + + // --- Error responses for new error codes --- + + @Test + fun testBadRequestError() { + val json = """{"result_type":"pay_invoice","error":{"code":"BAD_REQUEST","message":"Invalid invoice"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.BAD_REQUEST, response.error?.code) + } + + @Test + fun testNotFoundError() { + val json = """{"result_type":"lookup_invoice","error":{"code":"NOT_FOUND","message":"Invoice not found"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.NOT_FOUND, response.error?.code) + } + + @Test + fun testExpiredError() { + val json = """{"result_type":"pay_invoice","error":{"code":"EXPIRED","message":"Connection expired"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(NwcErrorCode.EXPIRED, response.error?.code) + } + // --- Null/missing result --- @Test diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestDeserializer.kt index 4098a5dc94..07e6eac965 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestDeserializer.kt @@ -25,7 +25,9 @@ import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionMethod import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetMethod import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoMethod import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceMethod @@ -36,6 +38,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendMethod import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageMethod import com.vitorpamplona.quartz.utils.asTextOrNull class RequestDeserializer : StdDeserializer(Request::class.java) { @@ -54,6 +57,9 @@ class RequestDeserializer : StdDeserializer(Request::class.java) { NwcMethod.LIST_TRANSACTIONS -> jp.codec.treeToValue(jsonObject, ListTransactionsMethod::class.java) NwcMethod.GET_BALANCE -> jp.codec.treeToValue(jsonObject, GetBalanceMethod::class.java) NwcMethod.GET_INFO -> jp.codec.treeToValue(jsonObject, GetInfoMethod::class.java) + NwcMethod.GET_BUDGET -> jp.codec.treeToValue(jsonObject, GetBudgetMethod::class.java) + NwcMethod.SIGN_MESSAGE -> jp.codec.treeToValue(jsonObject, SignMessageMethod::class.java) + NwcMethod.CREATE_CONNECTION -> jp.codec.treeToValue(jsonObject, CreateConnectionMethod::class.java) NwcMethod.MAKE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, MakeHoldInvoiceMethod::class.java) NwcMethod.CANCEL_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, CancelHoldInvoiceMethod::class.java) NwcMethod.SETTLE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, SettleHoldInvoiceMethod::class.java) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt index 72e7685fb8..c9b210ad95 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt @@ -25,7 +25,9 @@ import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceSuccessResponse @@ -39,6 +41,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageSuccessResponse import com.vitorpamplona.quartz.utils.asTextOrNull class ResponseDeserializer : StdDeserializer(Response::class.java) { @@ -70,6 +73,9 @@ class ResponseDeserializer : StdDeserializer(Response::class.java) { NwcMethod.LIST_TRANSACTIONS -> jp.codec.treeToValue(jsonObject, ListTransactionsSuccessResponse::class.java) NwcMethod.GET_BALANCE -> jp.codec.treeToValue(jsonObject, GetBalanceSuccessResponse::class.java) NwcMethod.GET_INFO -> jp.codec.treeToValue(jsonObject, GetInfoSuccessResponse::class.java) + NwcMethod.GET_BUDGET -> jp.codec.treeToValue(jsonObject, GetBudgetSuccessResponse::class.java) + NwcMethod.SIGN_MESSAGE -> jp.codec.treeToValue(jsonObject, SignMessageSuccessResponse::class.java) + NwcMethod.CREATE_CONNECTION -> jp.codec.treeToValue(jsonObject, CreateConnectionSuccessResponse::class.java) NwcMethod.MAKE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, MakeHoldInvoiceSuccessResponse::class.java) NwcMethod.CANCEL_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, CancelHoldInvoiceSuccessResponse::class.java) NwcMethod.SETTLE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, SettleHoldInvoiceSuccessResponse::class.java) From 4ee937ff0c1821caf7ffa8d6501446cb40e7021a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 03:00:56 +0000 Subject: [PATCH 070/172] test: add Alby Hub real test vectors to interop tests Add tests using exact JSON payloads from Alby Hub's test suite: - Real bolt11 invoice strings from Alby's mock data - pay_keysend with TLV records and preimage - make_invoice with nested metadata objects - make_hold_invoice with 64-char payment hash - settle_hold_invoice with preimage - list_transactions with unpaid_outgoing filter - create_connection with isolated=true https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../nip47WalletConnect/AlbyInteropTest.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt index babb67d670..8fea0211b0 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt @@ -309,6 +309,88 @@ class AlbyInteropTest { assertEquals(false, request.params?.isolated) } + // --- Alby Hub real bolt11 test vectors --- + + @Test + fun testAlbyRealBolt11PayInvoiceRequest() { + val json = + """{"method":"pay_invoice","params":{"invoice":"lntbs1230n1pnkqautdqyw3jsnp4q09a0z84kg4a2m38zjllw43h953fx5zvqe8qxfgw694ymkq26u8zcpp5yvnh6hsnlnj4xnuh2trzlnunx732dv8ta2wjr75pdfxf6p2vlyassp5hyeg97a3ft5u769kjwsn7p0e85h79pzz8kladmnqhpcypz2uawjs9qyysgqcqpcxq8zals8sq9yeg2pa9eywkgj50cyzxd5elatujuc0c0wh6j9nat5mn34pgk8u9ufpgs99tw9ldlfk42cqlkr48au3lmuh09269prg4qkggh4a8cyqpfl0y6j","metadata":{"a":123}}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertNotNull(request.params?.invoice) + assertNotNull(request.params?.metadata) + } + + @Test + fun testAlbyPayKeysendWithTlvRecords() { + val json = + """{"method":"pay_keysend","params":{"amount":123000,"pubkey":"123pubkey2","preimage":"018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b","tlv_records":[{"type":5482373484,"value":"fajsn341414fq"}]}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals(123000L, request.params?.amount) + assertEquals("123pubkey2", request.params?.pubkey) + assertEquals("018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b", request.params?.preimage) + assertNotNull(request.params?.tlv_records) + assertEquals(1, request.params?.tlv_records?.size) + assertEquals(5482373484L, request.params?.tlv_records?.first()?.type) + assertEquals("fajsn341414fq", request.params?.tlv_records?.first()?.value) + } + + @Test + fun testAlbyMakeInvoiceWithNestedMetadata() { + val json = + """{"method":"make_invoice","params":{"amount":1000,"description":"Hello, world","expiry":3600,"metadata":{"a":1,"b":"2","c":{"d":3,"e":[{"f":"g"},{"h":"i"}]}}}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals(1000L, request.params?.amount) + assertEquals("Hello, world", request.params?.description) + assertEquals(3600L, request.params?.expiry) + assertNotNull(request.params?.metadata) + } + + @Test + fun testAlbyMakeHoldInvoiceWithPaymentHash() { + val json = + """{"method":"make_hold_invoice","params":{"amount":1000,"description":"Hello, world","payment_hash":"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef","expiry":3600}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals(1000L, request.params?.amount) + assertEquals("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", request.params?.payment_hash) + assertEquals("Hello, world", request.params?.description) + } + + @Test + fun testAlbySettleHoldInvoiceWithPreimage() { + val json = + """{"method":"settle_hold_invoice","params":{"preimage":"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", request.params?.preimage) + } + + @Test + fun testAlbyListTransactionsWithUnpaidFilters() { + val json = """{"method":"list_transactions","params":{"from":0,"until":0,"limit":10,"offset":0,"unpaid_outgoing":true}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals(10, request.params?.limit) + assertEquals(true, request.params?.unpaid_outgoing) + } + + @Test + fun testAlbyCreateConnectionIsolated() { + val json = + """{"method":"create_connection","params":{"pubkey":"02a1633cafcc01ebfb6d78e39f687a1f0995c62fc95f51ead10a02ee0be551b5dc","name":"Test 123","request_methods":["get_info","pay_invoice"],"notification_types":["payment_received"],"max_amount":100000000,"budget_renewal":"monthly","isolated":true}}""" + val request = OptimizedJsonMapper.fromJsonTo(json) + assertIs(request) + assertEquals("Test 123", request.params?.name) + assertEquals(true, request.params?.isolated) + assertEquals(100000000L, request.params?.max_amount) + assertEquals("monthly", request.params?.budget_renewal) + assertEquals(listOf("get_info", "pay_invoice"), request.params?.request_methods) + assertEquals(listOf("payment_received"), request.params?.notification_types) + } + // --- Transaction with settle_deadline from Alby hold invoice --- @Test From 83a6feeca40b3b2c31a363487e8eec9e8c53c7c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 03:28:17 +0000 Subject: [PATCH 071/172] feat: add Alby JS SDK client interop improvements - Add NwcBudgetRenewal constants (daily/weekly/monthly/yearly/never) - Add case-insensitive NwcTransactionState helpers (isSettled, isPending, isFailed, isAccepted) for JS SDK lowercase state interop - Add URI test using Alby JS SDK test vector (69effe7b... pubkey) - Add JS SDK interop tests: lowercase transaction states in responses, notifications, and list_transactions; empty get_budget response; all budget renewal periods; structured transaction metadata; full 13-method get_info response https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../nip47WalletConnect/NwcTransaction.kt | 16 +++ .../nip47WalletConnect/AlbyInteropTest.kt | 101 +++++++++++++++++- .../Nip47WalletConnectTest.kt | 14 +++ .../nip47WalletConnect/NwcMethodTest.kt | 26 +++++ 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt index 9ba84991ef..067f12cd9c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt @@ -30,6 +30,22 @@ object NwcTransactionState { const val SETTLED = "SETTLED" const val FAILED = "FAILED" const val ACCEPTED = "ACCEPTED" + + fun isSettled(state: String?) = state.equals(SETTLED, ignoreCase = true) + + fun isPending(state: String?) = state.equals(PENDING, ignoreCase = true) + + fun isFailed(state: String?) = state.equals(FAILED, ignoreCase = true) + + fun isAccepted(state: String?) = state.equals(ACCEPTED, ignoreCase = true) +} + +object NwcBudgetRenewal { + const val DAILY = "daily" + const val WEEKLY = "weekly" + const val MONTHLY = "monthly" + const val YEARLY = "yearly" + const val NEVER = "never" } class NwcTransaction( diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt index 8fea0211b0..c201ab2850 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt @@ -26,10 +26,12 @@ import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue /** - * Interoperability tests using JSON structures matching Alby Hub's NIP-47 implementation. - * These tests verify that Amethyst can correctly parse responses from Alby Hub wallets. + * Interoperability tests using JSON structures matching Alby Hub (server) + * and Alby JS SDK (client) NIP-47 implementations. + * These tests verify that Amethyst can correctly parse responses from Alby wallets. */ class AlbyInteropTest { // --- Alby Hub pay_invoice response format --- @@ -404,4 +406,99 @@ class AlbyInteropTest { assertEquals(144L, txn.settle_deadline) assertEquals(NwcTransactionState.PENDING, txn.state) } + + // =================================================================== + // Alby JS SDK (client) interop tests + // The JS SDK uses lowercase transaction states while Hub uses uppercase. + // Both formats must be handled correctly. + // =================================================================== + + @Test + fun testJsSdkLowercaseSettledState() { + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash123","amount":1000,"settled_at":1694876497}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("settled", response.result?.state) + assertTrue(NwcTransactionState.isSettled(response.result?.state)) + } + + @Test + fun testJsSdkLowercasePendingState() { + val json = + """{"result_type":"make_invoice","result":{"type":"incoming","state":"pending","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals("pending", response.result?.state) + assertTrue(NwcTransactionState.isPending(response.result?.state)) + } + + @Test + fun testJsSdkLowercaseStatesInListTransactions() { + val json = + """{"result_type":"list_transactions","result":{"transactions":[{"type":"incoming","state":"settled","amount":1000,"created_at":1000},{"type":"outgoing","state":"failed","amount":2000,"created_at":2000},{"type":"incoming","state":"accepted","amount":3000,"created_at":3000}],"total_count":3}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val txns = response.result?.transactions + assertNotNull(txns) + assertEquals(3, txns.size) + assertTrue(NwcTransactionState.isSettled(txns[0].state)) + assertTrue(NwcTransactionState.isFailed(txns[1].state)) + assertTrue(NwcTransactionState.isAccepted(txns[2].state)) + } + + @Test + fun testJsSdkLowercaseStatesInNotification() { + val json = + """{"notification_type":"payment_received","notification":{"type":"incoming","state":"settled","invoice":"lnbc...","amount":5000,"created_at":1000,"settled_at":2000}}""" + val notification = OptimizedJsonMapper.fromJsonTo(json) + assertIs(notification) + assertTrue(NwcTransactionState.isSettled(notification.notification?.state)) + } + + @Test + fun testJsSdkEmptyGetBudgetResponse() { + // JS SDK allows get_budget to return empty object when no budget is set + val json = """{"result_type":"get_budget","result":{}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNull(response.result?.used_budget) + assertNull(response.result?.total_budget) + assertNull(response.result?.renews_at) + assertNull(response.result?.renewal_period) + } + + @Test + fun testJsSdkGetBudgetWithAllRenewalPeriods() { + for (period in listOf("daily", "weekly", "monthly", "yearly", "never")) { + val json = """{"result_type":"get_budget","result":{"used_budget":0,"total_budget":100000,"renewal_period":"$period"}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(period, response.result?.renewal_period) + } + } + + @Test + fun testJsSdkTransactionWithMetadata() { + // JS SDK supports structured metadata with comment, payer_data, nostr fields + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"settled_at":2000,"metadata":{"comment":"Thanks!","payer_data":{"name":"Alice","pubkey":"abc123"},"nostr":{"pubkey":"npub1...","tags":[["p","def456"]]}}}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertNotNull(response.result?.metadata) + } + + @Test + fun testJsSdkGetInfoWithAllMethods() { + // JS SDK advertises all 13 single methods + notifications + val json = + """{"result_type":"get_info","result":{"alias":"TestNode","methods":["get_info","get_balance","get_budget","make_invoice","pay_invoice","pay_keysend","lookup_invoice","list_transactions","sign_message","create_connection","make_hold_invoice","settle_hold_invoice","cancel_hold_invoice"],"notifications":["payment_received","payment_sent","hold_invoice_accepted"]}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + assertEquals(13, response.result?.methods?.size) + assertEquals(3, response.result?.notifications?.size) + assertTrue(response.result?.methods?.contains(NwcMethod.GET_BUDGET) == true) + assertTrue(response.result?.methods?.contains(NwcMethod.SIGN_MESSAGE) == true) + assertTrue(response.result?.methods?.contains(NwcMethod.CREATE_CONNECTION) == true) + } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnectTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnectTest.kt index c31c376d71..825069de6b 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnectTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47WalletConnectTest.kt @@ -93,6 +93,20 @@ class Nip47WalletConnectTest { } } + // --- Alby JS SDK URI test vector --- + + @Test + fun testParseAlbyJsSdkUri() { + // Test vector from @getalby/js-sdk NWCClient.test.ts + val uri = + "nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss%3A%2F%2Frelay.getalby.com%2Fv1&secret=e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b&lud16=hello%40getalby.com" + val parsed = Nip47WalletConnect.parse(uri) + + assertEquals("69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9", parsed.pubKeyHex) + assertEquals("e839faf78693765b3833027fefa5a305c78f6965d0a5d2e47a3fcb25aa7cc45b", parsed.secret) + assertEquals("hello@getalby.com", parsed.lud16) + } + // --- Nip47URI serialization --- @Test diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt index 4d76bb9261..f8806444b1 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcMethodTest.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.quartz.nip47WalletConnect import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class NwcMethodTest { @Test @@ -81,6 +83,30 @@ class NwcMethodTest { assertEquals("ACCEPTED", NwcTransactionState.ACCEPTED) } + @Test + fun testTransactionStateCaseInsensitive() { + // Alby Hub uses uppercase, Alby JS SDK uses lowercase + assertTrue(NwcTransactionState.isSettled("SETTLED")) + assertTrue(NwcTransactionState.isSettled("settled")) + assertTrue(NwcTransactionState.isPending("PENDING")) + assertTrue(NwcTransactionState.isPending("pending")) + assertTrue(NwcTransactionState.isFailed("FAILED")) + assertTrue(NwcTransactionState.isFailed("failed")) + assertTrue(NwcTransactionState.isAccepted("ACCEPTED")) + assertTrue(NwcTransactionState.isAccepted("accepted")) + assertFalse(NwcTransactionState.isSettled("pending")) + assertFalse(NwcTransactionState.isSettled(null)) + } + + @Test + fun testBudgetRenewalConstants() { + assertEquals("daily", NwcBudgetRenewal.DAILY) + assertEquals("weekly", NwcBudgetRenewal.WEEKLY) + assertEquals("monthly", NwcBudgetRenewal.MONTHLY) + assertEquals("yearly", NwcBudgetRenewal.YEARLY) + assertEquals("never", NwcBudgetRenewal.NEVER) + } + @Test fun testNwcError() { val error = NwcError(NwcErrorCode.UNAUTHORIZED, "not allowed") From 4b76c70c29fcce8ecd9402afc8c6141c5f23ea86 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 03:52:41 +0000 Subject: [PATCH 072/172] feat: add NIP-47 wallet interface with balance, send, receive, and transactions Adds an Alby Go-inspired wallet interface accessible from the left drawer menu. Uses existing NWC connection from zap settings to communicate with the wallet via NIP-47 methods (get_balance, get_info, pay_invoice, make_invoice, list_transactions). New files: - WalletViewModel: manages wallet state and NWC requests - WalletScreen: balance display with send/receive action buttons - WalletSendScreen: paste BOLT-11 invoice and pay - WalletReceiveScreen: create invoice with QR code display - WalletTransactionsScreen: scrollable transaction history Also adds generic sendNwcRequest() to NwcSignerState and Account for sending arbitrary NIP-47 requests beyond just pay_invoice. https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../vitorpamplona/amethyst/model/Account.kt | 9 + .../nip47WalletConnect/NwcSignerState.kt | 39 +++ .../amethyst/ui/navigation/AppNavigation.kt | 9 + .../ui/navigation/drawer/DrawerContent.kt | 9 + .../amethyst/ui/navigation/routes/Routes.kt | 8 + .../loggedIn/wallet/WalletReceiveScreen.kt | 268 +++++++++++++++++ .../ui/screen/loggedIn/wallet/WalletScreen.kt | 279 ++++++++++++++++++ .../loggedIn/wallet/WalletSendScreen.kt | 219 ++++++++++++++ .../wallet/WalletTransactionsScreen.kt | 226 ++++++++++++++ .../screen/loggedIn/wallet/WalletViewModel.kt | 264 +++++++++++++++++ amethyst/src/main/res/values/strings.xml | 23 ++ 11 files changed, 1353 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 055cb6ae28..ca79bcf40e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -171,6 +171,7 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftEventCache import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect +import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -581,6 +582,14 @@ class Account( suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState) + suspend fun sendNwcRequest( + request: Request, + onResponse: (Response?) -> Unit, + ) { + val (event, relay) = nip47SignerState.sendNwcRequest(request, onResponse) + client.send(event, setOf(relay)) + } + suspend fun sendZapPaymentRequestFor( bolt11: String, zappedNote: Note?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index d4f012cbbe..206f9bbfd6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -138,6 +138,45 @@ class NwcSignerState( return zapPaymentResponseDecryptionCache.value.decryptResponse(event) } + /** + * Sends a generic NIP-47 request to the connected wallet. + * Subscribes to responses and waits up to 60s for a reply. + * + * @param request the NIP-47 request to send + * @param onResponse callback to handle the response from the wallet + * @return a pair containing the request event and target relay URL + * @throws IllegalArgumentException if no NIP-47 wallet is set up + */ + suspend fun sendNwcRequest( + request: Request, + onResponse: (Response?) -> Unit, + ): Pair { + val walletService = nip47Setup.value ?: throw IllegalArgumentException("No NIP47 setup") + + val event = LnZapPaymentRequestEvent.createRequest(request, walletService.pubKeyHex, nip47Signer.value) + + val filter = + NWCPaymentQueryState( + fromServiceHex = walletService.pubKeyHex, + toUserHex = event.pubKey, + replyingToHex = event.id, + relay = walletService.relayUri, + ) + + nwcFilterAssembler.subscribe(filter) + + scope.launch(Dispatchers.IO) { + delay(60000) + nwcFilterAssembler.unsubscribe(filter) + } + + cache.consume(event, null, true, walletService.relayUri) { + onResponse(decryptResponse(it)) + } + + return Pair(event, walletService.relayUri) + } + /** * Sends a zap payment request to a connected Lightning wallet. * Subscribes to responses and waits up to 60s for a reply. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 2bb646e982..c4d9d37e60 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -119,6 +119,10 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UpdateZapAmountScr import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.UserSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.ThreadScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.video.VideoScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletReceiveScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletSendScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet.WalletTransactionsScreen import com.vitorpamplona.amethyst.ui.screen.loggedOff.AddAccountDialog import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId @@ -151,6 +155,11 @@ fun AppNavigation( composable { NotificationScreen(accountViewModel, nav) } composable { ChessLobbyScreen(accountViewModel, nav) } + composableFromEnd { WalletScreen(accountViewModel, nav) } + composableFromEnd { WalletSendScreen(accountViewModel, nav) } + composableFromEnd { WalletReceiveScreen(accountViewModel, nav) } + composableFromEnd { WalletTransactionsScreen(accountViewModel, nav) } + composableFromEnd { ListOfPeopleListsScreen(accountViewModel, nav) } composableFromEndArgs { PeopleListScreen(it.dTag, accountViewModel, nav) } composableFromEndArgs { FollowPackScreen(it.dTag, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index 8ca73fb972..a3682c723d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -49,6 +49,7 @@ import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.Drafts import androidx.compose.material.icons.outlined.GroupAdd +import androidx.compose.material.icons.outlined.AccountBalanceWallet import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -463,6 +464,14 @@ fun ListContent( route = Route.Drafts, ) + NavigationRow( + title = R.string.wallet, + icon = Icons.Outlined.AccountBalanceWallet, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Wallet, + ) + NavigationRow( title = R.string.route_chess, icon = R.drawable.ic_chess, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index b55b4dd063..b7191759c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -43,6 +43,14 @@ sealed class Route { @Serializable object Chess : Route() + @Serializable object Wallet : Route() + + @Serializable object WalletSend : Route() + + @Serializable object WalletReceive : Route() + + @Serializable object WalletTransactions : Route() + @Serializable object Search : Route() @Serializable object SecurityFilters : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt new file mode 100644 index 0000000000..c0451ede7a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletReceiveScreen.kt @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.QrCodeDrawer +import com.vitorpamplona.amethyst.ui.stringRes +import java.text.NumberFormat + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WalletReceiveScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + val walletViewModel: WalletViewModel = viewModel() + + LaunchedEffect(accountViewModel) { + walletViewModel.init(accountViewModel.account) + } + + DisposableEffect(Unit) { + onDispose { walletViewModel.resetReceiveState() } + } + + val receiveState by walletViewModel.receiveState.collectAsState() + var amountText by remember { mutableStateOf("") } + var descriptionText by remember { mutableStateOf("") } + val context = LocalContext.current + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringRes(R.string.wallet_receive)) }, + navigationIcon = { + IconButton(onClick = { nav.popBack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringRes(R.string.back), + ) + } + }, + ) + }, + ) { padding -> + Column( + modifier = + Modifier + .padding(padding) + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (val state = receiveState) { + is ReceiveState.Idle -> { + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = amountText, + onValueChange = { amountText = it.filter { c -> c.isDigit() } }, + label = { Text(stringRes(R.string.wallet_amount_sats)) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + singleLine = true, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedTextField( + value = descriptionText, + onValueChange = { descriptionText = it }, + label = { Text(stringRes(R.string.wallet_description)) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + singleLine = true, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = { + val amount = amountText.toLongOrNull() + if (amount != null && amount > 0) { + walletViewModel.createInvoice( + amountSats = amount, + description = descriptionText.ifBlank { null }, + ) + } + }, + modifier = + Modifier + .fillMaxWidth() + .height(56.dp), + shape = RoundedCornerShape(16.dp), + enabled = amountText.isNotBlank() && (amountText.toLongOrNull() ?: 0L) > 0, + ) { + Text( + stringRes(R.string.wallet_create_invoice), + fontWeight = FontWeight.SemiBold, + ) + } + } + + is ReceiveState.Creating -> { + Spacer(modifier = Modifier.weight(1f)) + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + Spacer(modifier = Modifier.height(16.dp)) + Text( + stringRes(R.string.wallet_creating_invoice), + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.weight(1f)) + } + + is ReceiveState.Created -> { + Spacer(modifier = Modifier.height(8.dp)) + + val formattedAmount = + remember(state.amount) { + val fmt = NumberFormat.getIntegerInstance() + fmt.format(state.amount) + } + + Text( + text = "$formattedAmount ${stringRes(R.string.wallet_sats)}", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + QrCodeDrawer( + contents = state.invoice, + modifier = + Modifier + .fillMaxWidth() + .weight(1f), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = state.invoice, + style = MaterialTheme.typography.bodySmall, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + OutlinedButton( + onClick = { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText("invoice", state.invoice)) + }, + modifier = + Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.Filled.ContentCopy, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringRes(R.string.wallet_copy_invoice)) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + } + + is ReceiveState.Error -> { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + state.message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { walletViewModel.resetReceiveState() }) { + Text(stringRes(R.string.back)) + } + } + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt new file mode 100644 index 0000000000..ef0d948000 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.routes.Route +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import java.text.NumberFormat + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WalletScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + val walletViewModel: WalletViewModel = viewModel() + + LaunchedEffect(accountViewModel) { + walletViewModel.init(accountViewModel.account) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringRes(R.string.wallet)) }, + navigationIcon = { + IconButton(onClick = { nav.popBack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringRes(R.string.back), + ) + } + }, + ) + }, + ) { padding -> + if (!walletViewModel.hasWalletSetup()) { + NoWalletSetup( + modifier = Modifier.padding(padding), + nav = nav, + ) + } else { + WalletHomeContent( + walletViewModel = walletViewModel, + modifier = Modifier.padding(padding), + nav = nav, + ) + } + } +} + +@Composable +private fun NoWalletSetup( + modifier: Modifier, + nav: INav, +) { + Column( + modifier = + modifier + .fillMaxSize() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringRes(R.string.wallet_no_connection), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = stringRes(R.string.wallet_no_connection_description), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(24.dp)) + Button(onClick = { nav.nav(Route.Nip47NWCSetup()) }) { + Text(stringRes(R.string.wallet_setup)) + } + } +} + +@Composable +private fun WalletHomeContent( + walletViewModel: WalletViewModel, + modifier: Modifier, + nav: INav, +) { + val balance by walletViewModel.balanceSats.collectAsState() + val walletAlias by walletViewModel.walletAlias.collectAsState() + val isLoading by walletViewModel.isLoading.collectAsState() + val error by walletViewModel.error.collectAsState() + + LaunchedEffect(Unit) { + walletViewModel.fetchBalance() + walletViewModel.fetchInfo() + } + + Column( + modifier = + modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(32.dp)) + + // Wallet name + if (walletAlias != null) { + Text( + text = walletAlias!!, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + // Balance display + if (isLoading && balance == null) { + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + } else { + val formattedBalance = + remember(balance) { + val fmt = NumberFormat.getIntegerInstance() + fmt.format(balance ?: 0L) + } + Text( + text = formattedBalance, + fontSize = 48.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = stringRes(R.string.wallet_sats), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + // Error + if (error != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = error!!, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + // Action buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Button( + onClick = { nav.nav(Route.WalletReceive) }, + modifier = + Modifier + .weight(1f) + .height(56.dp), + shape = RoundedCornerShape(16.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) { + Icon( + imageVector = Icons.Filled.ArrowDownward, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringRes(R.string.wallet_receive), fontWeight = FontWeight.SemiBold) + } + + Button( + onClick = { nav.nav(Route.WalletSend) }, + modifier = + Modifier + .weight(1f) + .height(56.dp), + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.Filled.ArrowUpward, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringRes(R.string.wallet_send), fontWeight = FontWeight.SemiBold) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Transactions button + OutlinedButton( + onClick = { nav.nav(Route.WalletTransactions) }, + modifier = + Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(16.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.List, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringRes(R.string.wallet_transactions)) + } + + Spacer(modifier = Modifier.height(24.dp)) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt new file mode 100644 index 0000000000..ca5bd26096 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletSendScreen.kt @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet + +import android.content.ClipboardManager +import android.content.Context +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.ContentPaste +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WalletSendScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + val walletViewModel: WalletViewModel = viewModel() + + LaunchedEffect(accountViewModel) { + walletViewModel.init(accountViewModel.account) + } + + DisposableEffect(Unit) { + onDispose { walletViewModel.resetSendState() } + } + + val sendState by walletViewModel.sendState.collectAsState() + var invoiceText by remember { mutableStateOf("") } + val context = LocalContext.current + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringRes(R.string.wallet_send)) }, + navigationIcon = { + IconButton(onClick = { nav.popBack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringRes(R.string.back), + ) + } + }, + ) + }, + ) { padding -> + Column( + modifier = + Modifier + .padding(padding) + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (val state = sendState) { + is SendState.Idle -> { + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = invoiceText, + onValueChange = { invoiceText = it }, + label = { Text(stringRes(R.string.wallet_paste_invoice)) }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(12.dp), + minLines = 3, + maxLines = 5, + trailingIcon = { + IconButton(onClick = { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = clipboard.primaryClip + if (clip != null && clip.itemCount > 0) { + invoiceText = clip.getItemAt(0).text?.toString() ?: "" + } + }) { + Icon( + imageVector = Icons.Filled.ContentPaste, + contentDescription = "Paste", + ) + } + }, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button( + onClick = { + if (invoiceText.isNotBlank()) { + walletViewModel.sendPayment(invoiceText.trim()) + } + }, + modifier = + Modifier + .fillMaxWidth() + .height(56.dp), + shape = RoundedCornerShape(16.dp), + enabled = invoiceText.isNotBlank(), + ) { + Text( + stringRes(R.string.wallet_pay), + fontWeight = FontWeight.SemiBold, + ) + } + } + + is SendState.Sending -> { + Spacer(modifier = Modifier.weight(1f)) + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + Spacer(modifier = Modifier.height(16.dp)) + Text( + stringRes(R.string.wallet_payment_sending), + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.weight(1f)) + } + + is SendState.Success -> { + Spacer(modifier = Modifier.weight(1f)) + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + stringRes(R.string.wallet_payment_success), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + ) + Spacer(modifier = Modifier.weight(1f)) + Button( + onClick = { nav.popBack() }, + modifier = + Modifier + .fillMaxWidth() + .height(56.dp), + shape = RoundedCornerShape(16.dp), + ) { + Text(stringRes(R.string.back)) + } + Spacer(modifier = Modifier.height(24.dp)) + } + + is SendState.Error -> { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + state.message, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(16.dp)) + Button(onClick = { walletViewModel.resetSendState() }) { + Text(stringRes(R.string.back)) + } + } + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt new file mode 100644 index 0000000000..a7df6653b8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material.icons.filled.ArrowUpward +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransaction +import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransactionType +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WalletTransactionsScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + val walletViewModel: WalletViewModel = viewModel() + + LaunchedEffect(accountViewModel) { + walletViewModel.init(accountViewModel.account) + walletViewModel.fetchTransactions() + } + + val transactions by walletViewModel.transactions.collectAsState() + val isLoading by walletViewModel.isLoading.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringRes(R.string.wallet_transactions)) }, + navigationIcon = { + IconButton(onClick = { nav.popBack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringRes(R.string.back), + ) + } + }, + actions = { + IconButton(onClick = { walletViewModel.fetchTransactions() }) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringRes(R.string.wallet_refresh), + ) + } + }, + ) + }, + ) { padding -> + if (isLoading && transactions.isEmpty()) { + Column( + modifier = + Modifier + .padding(padding) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + Spacer(modifier = Modifier.height(16.dp)) + Text( + stringRes(R.string.wallet_loading), + style = MaterialTheme.typography.bodyLarge, + ) + } + } else if (transactions.isEmpty()) { + Column( + modifier = + Modifier + .padding(padding) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + stringRes(R.string.wallet_no_transactions), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } else { + LazyColumn( + modifier = Modifier.padding(padding), + ) { + items(transactions) { tx -> + TransactionItem(tx) + HorizontalDivider() + } + } + } + } +} + +@Composable +private fun TransactionItem(tx: NwcTransaction) { + val isIncoming = tx.type == NwcTransactionType.INCOMING + val amountSats = (tx.amount ?: 0L) / 1000L + val formattedAmount = + remember(amountSats) { + val fmt = NumberFormat.getIntegerInstance() + (if (isIncoming) "+" else "-") + fmt.format(amountSats) + } + + val dateText = + remember(tx.created_at) { + tx.created_at?.let { + val sdf = SimpleDateFormat("MMM d, HH:mm", Locale.getDefault()) + sdf.format(Date(it * 1000L)) + } ?: "" + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = + if (isIncoming) Icons.Filled.ArrowDownward else Icons.Filled.ArrowUpward, + contentDescription = + if (isIncoming) { + stringRes(R.string.wallet_incoming) + } else { + stringRes(R.string.wallet_outgoing) + }, + modifier = Modifier.size(24.dp), + tint = + if (isIncoming) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = tx.description ?: if (isIncoming) stringRes(R.string.wallet_incoming) else stringRes(R.string.wallet_outgoing), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = dateText, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + Text( + text = "$formattedAmount sats", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = + if (isIncoming) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onBackground + }, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt new file mode 100644 index 0000000000..817c296e29 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.wallet + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoMethod +import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse +import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransaction +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +sealed class SendState { + data object Idle : SendState() + + data object Sending : SendState() + + data class Success( + val preimage: String?, + ) : SendState() + + data class Error( + val message: String, + ) : SendState() +} + +sealed class ReceiveState { + data object Idle : ReceiveState() + + data object Creating : ReceiveState() + + data class Created( + val invoice: String, + val amount: Long, + ) : ReceiveState() + + data class Error( + val message: String, + ) : ReceiveState() +} + +class WalletViewModel : ViewModel() { + private var account: Account? = null + + private val _balanceSats = MutableStateFlow(null) + val balanceSats = _balanceSats.asStateFlow() + + private val _walletAlias = MutableStateFlow(null) + val walletAlias = _walletAlias.asStateFlow() + + private val _transactions = MutableStateFlow>(emptyList()) + val transactions = _transactions.asStateFlow() + + private val _isLoading = MutableStateFlow(false) + val isLoading = _isLoading.asStateFlow() + + private val _error = MutableStateFlow(null) + val error = _error.asStateFlow() + + private val _sendState = MutableStateFlow(SendState.Idle) + val sendState = _sendState.asStateFlow() + + private val _receiveState = MutableStateFlow(ReceiveState.Idle) + val receiveState = _receiveState.asStateFlow() + + fun init(account: Account) { + this.account = account + } + + fun hasWalletSetup(): Boolean = account?.nip47SignerState?.hasWalletConnectSetup() == true + + fun fetchBalance() { + val acc = account ?: return + viewModelScope.launch(Dispatchers.IO) { + _isLoading.value = true + _error.value = null + try { + acc.sendNwcRequest(GetBalanceMethod.create()) { response -> + when (response) { + is GetBalanceSuccessResponse -> { + // NWC balance is in millisats, convert to sats + _balanceSats.value = (response.result?.balance ?: 0L) / 1000L + } + is NwcErrorResponse -> { + _error.value = response.error?.message ?: "Balance request failed" + } + else -> {} + } + _isLoading.value = false + } + } catch (e: Exception) { + _error.value = e.message + _isLoading.value = false + } + } + } + + fun fetchInfo() { + val acc = account ?: return + viewModelScope.launch(Dispatchers.IO) { + try { + acc.sendNwcRequest(GetInfoMethod.create()) { response -> + when (response) { + is GetInfoSuccessResponse -> { + _walletAlias.value = response.result?.alias + } + else -> {} + } + } + } catch (e: Exception) { + // ignore info errors + } + } + } + + fun fetchTransactions( + limit: Int = 20, + offset: Int = 0, + ) { + val acc = account ?: return + viewModelScope.launch(Dispatchers.IO) { + _isLoading.value = true + try { + acc.sendNwcRequest( + ListTransactionsMethod.create( + limit = limit, + offset = offset, + unpaid = false, + ), + ) { response -> + when (response) { + is ListTransactionsSuccessResponse -> { + _transactions.value = response.result?.transactions ?: emptyList() + } + is NwcErrorResponse -> { + _error.value = response.error?.message ?: "Failed to load transactions" + } + else -> {} + } + _isLoading.value = false + } + } catch (e: Exception) { + _error.value = e.message + _isLoading.value = false + } + } + } + + fun sendPayment(bolt11: String) { + val acc = account ?: return + viewModelScope.launch(Dispatchers.IO) { + _sendState.value = SendState.Sending + try { + acc.sendNwcRequest(PayInvoiceMethod.create(bolt11)) { response -> + when (response) { + is PayInvoiceSuccessResponse -> { + _sendState.value = SendState.Success(response.result?.preimage) + // Refresh balance after payment + fetchBalance() + } + is PayInvoiceErrorResponse -> { + _sendState.value = SendState.Error( + response.error?.message ?: "Payment failed", + ) + } + is NwcErrorResponse -> { + _sendState.value = SendState.Error( + response.error?.message ?: "Payment failed", + ) + } + else -> { + _sendState.value = SendState.Error("Unexpected response") + } + } + } + } catch (e: Exception) { + _sendState.value = SendState.Error(e.message ?: "Payment failed") + } + } + } + + fun createInvoice( + amountSats: Long, + description: String? = null, + ) { + val acc = account ?: return + viewModelScope.launch(Dispatchers.IO) { + _receiveState.value = ReceiveState.Creating + try { + // NWC expects millisats + acc.sendNwcRequest( + MakeInvoiceMethod.create( + amount = amountSats * 1000L, + description = description, + ), + ) { response -> + when (response) { + is MakeInvoiceSuccessResponse -> { + val invoice = response.result?.invoice + if (invoice != null) { + _receiveState.value = ReceiveState.Created(invoice, amountSats) + } else { + _receiveState.value = ReceiveState.Error("No invoice returned") + } + } + is NwcErrorResponse -> { + _receiveState.value = ReceiveState.Error( + response.error?.message ?: "Invoice creation failed", + ) + } + else -> { + _receiveState.value = ReceiveState.Error("Unexpected response") + } + } + } + } catch (e: Exception) { + _receiveState.value = ReceiveState.Error(e.message ?: "Invoice creation failed") + } + } + } + + fun resetSendState() { + _sendState.value = SendState.Idle + } + + fun resetReceiveState() { + _receiveState.value = ReceiveState.Idle + } + + fun clearError() { + _error.value = null + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 72f3724dc7..48d86db712 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1236,6 +1236,29 @@ Global Shorts Chess + Wallet + Balance + Send + Receive + Transactions + No wallet connected + Set up a Nostr Wallet Connect (NWC) connection in your zap settings to use the wallet. + Set Up Wallet + sats + Paste a BOLT-11 invoice + Pay + Payment successful + Sending payment… + Amount (sats) + Description (optional) + Create Invoice + Creating invoice… + Copy Invoice + No transactions yet + Loading… + Received + Sent + Refresh Security Filters Import Follows From 580678eed86a93d50e50f1b555e635eb970baee8 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 04:25:27 +0000 Subject: [PATCH 073/172] feat: add server-side event builders and NIP-47 README for quartz Adds missing server-side capabilities to the NIP-47 quartz module: - LnZapPaymentResponseEvent.createResponse(): builds encrypted response events (kind 23195) for wallet services to reply to client requests - NwcNotificationEvent.createNotification(): builds encrypted notification events (kind 23197) for wallet services to push payment notifications Creates comprehensive README.md documenting the full NIP-47 API with code examples for both wallet client and wallet service implementations, covering URI parsing, request/response building, encryption, notifications, error handling, transaction states, and caching. https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../LnZapPaymentResponseEvent.kt | 41 +- .../NwcNotificationEvent.kt | 30 ++ .../quartz/nip47WalletConnect/README.md | 480 ++++++++++++++++++ 3 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt index 9a7de947cb..a75ba45e80 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentResponseEvent.kt @@ -26,6 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class LnZapPaymentResponseEvent( @@ -55,6 +57,43 @@ class LnZapPaymentResponseEvent( companion object { const val KIND = 23195 - const val ALT = "Zap payment response" + const val ALT = "NWC response" + + /** + * Creates an NWC response event (server-side). + * + * @param response the NWC response object to send + * @param requestEvent the original request event being responded to + * @param signer the wallet service signer + * @param useNip44 whether to use NIP-44 encryption (default: false for NIP-04) + * @param createdAt event timestamp + */ + suspend fun createResponse( + response: Response, + requestEvent: LnZapPaymentRequestEvent, + signer: NostrSigner, + useNip44: Boolean = false, + createdAt: Long = TimeUtils.now(), + ): LnZapPaymentResponseEvent { + val serializedResponse = OptimizedJsonMapper.toJson(response) + + val clientPubkey = requestEvent.pubKey + + val tags = + arrayOf( + arrayOf("p", clientPubkey), + arrayOf("e", requestEvent.id), + AltTag.assemble(ALT), + ) + + val encrypted = + if (useNip44) { + signer.nip44Encrypt(serializedResponse, clientPubkey) + } else { + signer.nip04Encrypt(serializedResponse, clientPubkey) + } + + return signer.sign(createdAt, KIND, tags, encrypted) + } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt index 77b16a3ec6..9e9af6b2cb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt @@ -26,6 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions +import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.utils.TimeUtils @Immutable class NwcNotificationEvent( @@ -54,5 +56,33 @@ class NwcNotificationEvent( const val KIND = 23197 const val LEGACY_KIND = 23196 const val ALT = "Wallet notification" + + /** + * Creates an NWC notification event (server-side). + * Uses NIP-44 encryption (kind 23197). + * + * @param notification the notification to send + * @param clientPubkey the client's public key to encrypt to + * @param signer the wallet service signer + * @param createdAt event timestamp + */ + suspend fun createNotification( + notification: Notification, + clientPubkey: HexKey, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + ): NwcNotificationEvent { + val serialized = OptimizedJsonMapper.toJson(notification) + + val tags = + arrayOf( + arrayOf("p", clientPubkey), + AltTag.assemble(ALT), + ) + + val encrypted = signer.nip44Encrypt(serialized, clientPubkey) + + return signer.sign(createdAt, KIND, tags, encrypted) + } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md new file mode 100644 index 0000000000..95907efa97 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md @@ -0,0 +1,480 @@ +# NIP-47 Wallet Connect (Quartz) + +Quartz implementation of [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) — Nostr +Wallet Connect (NWC). This module provides everything needed to build both **wallet client apps** +(like Amethyst) and **wallet service backends** (like Alby Hub). + +## Architecture + +``` +nip47WalletConnect/ +├── Nip47WalletConnect.kt # URI parsing (nostr+walletconnect://) +├── Request.kt # All 13 NWC request methods + params +├── Response.kt # All response types (success + error) +├── Notification.kt # Wallet notification types +├── NwcMethod.kt # Method name constants +├── NwcErrorCode.kt # Error codes enum + NwcError +├── NwcTransaction.kt # Transaction, state, budget, TLV models +├── NwcInfoEvent.kt # Kind 13194 — wallet capabilities +├── LnZapPaymentRequestEvent.kt # Kind 23194 — client → wallet request +├── LnZapPaymentResponseEvent.kt # Kind 23195 — wallet → client response +├── NwcNotificationEvent.kt # Kind 23197 — wallet → client notification +├── NostrWalletConnectRequestCache.kt # Request decryption cache +├── NostrWalletConnectResponseCache.kt # Response decryption cache +└── tags/ + ├── EncryptionTag.kt # "encryption" tag parsing + └── NotificationsTag.kt # "notifications" tag parsing +``` + +## Event Kinds + +| Kind | Class | Direction | Purpose | +|-------|------------------------------|-----------------|----------------------| +| 13194 | `NwcInfoEvent` | Wallet → Relay | Service capabilities | +| 23194 | `LnZapPaymentRequestEvent` | Client → Wallet | NWC request | +| 23195 | `LnZapPaymentResponseEvent` | Wallet → Client | NWC response | +| 23196 | `NwcNotificationEvent` | Wallet → Client | Notification (NIP-04, legacy) | +| 23197 | `NwcNotificationEvent` | Wallet → Client | Notification (NIP-44) | + +## Supported Methods + +| Method | Request Class | Success Response Class | +|----------------------|---------------------------|-----------------------------------| +| `pay_invoice` | `PayInvoiceMethod` | `PayInvoiceSuccessResponse` | +| `pay_keysend` | `PayKeysendMethod` | `PayKeysendSuccessResponse` | +| `make_invoice` | `MakeInvoiceMethod` | `MakeInvoiceSuccessResponse` | +| `lookup_invoice` | `LookupInvoiceMethod` | `LookupInvoiceSuccessResponse` | +| `list_transactions` | `ListTransactionsMethod` | `ListTransactionsSuccessResponse` | +| `get_balance` | `GetBalanceMethod` | `GetBalanceSuccessResponse` | +| `get_info` | `GetInfoMethod` | `GetInfoSuccessResponse` | +| `get_budget` | `GetBudgetMethod` | `GetBudgetSuccessResponse` | +| `sign_message` | `SignMessageMethod` | `SignMessageSuccessResponse` | +| `create_connection` | `CreateConnectionMethod` | `CreateConnectionSuccessResponse` | +| `make_hold_invoice` | `MakeHoldInvoiceMethod` | `MakeHoldInvoiceSuccessResponse` | +| `cancel_hold_invoice`| `CancelHoldInvoiceMethod` | `CancelHoldInvoiceSuccessResponse`| +| `settle_hold_invoice`| `SettleHoldInvoiceMethod` | `SettleHoldInvoiceSuccessResponse`| + +Any method can also return `NwcErrorResponse` or (for `pay_invoice`) `PayInvoiceErrorResponse`. + +## Implementing a Wallet Client + +A wallet client connects to a user's lightning wallet to send payments, check +balances, create invoices, and list transactions. + +### 1. Parse the NWC Connection URI + +Users provide an NWC connection string from their wallet provider: + +```kotlin +val uri = "nostr+walletconnect://b889ff5b...?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c..." +val nwcConfig = Nip47WalletConnect.parse(uri) + +// nwcConfig.pubKeyHex — wallet service pubkey +// nwcConfig.relayUri — relay to communicate through (NormalizedRelayUrl) +// nwcConfig.secret — hex secret for the client signer +// nwcConfig.lud16 — optional lightning address +``` + +Supported URI schemes: `nostr+walletconnect://`, `nostrwalletconnect://`, +`amethyst+walletconnect://` + +### 2. Create the Client Signer + +The `secret` from the URI becomes the client's signing key: + +```kotlin +val clientSigner = NostrSignerInternal( + KeyPair(nwcConfig.secret!!.hexToByteArray()) +) +``` + +### 3. Build and Send Requests + +Use `LnZapPaymentRequestEvent.createRequest()` to create encrypted request events: + +```kotlin +// Get balance +val balanceRequest = GetBalanceMethod.create() +val event = LnZapPaymentRequestEvent.createRequest( + request = balanceRequest, + walletServicePubkey = nwcConfig.pubKeyHex, + signer = clientSigner, +) +// Send `event` to `nwcConfig.relayUri` + +// Pay an invoice +val payRequest = PayInvoiceMethod.create("lnbc50n1...") +val payEvent = LnZapPaymentRequestEvent.createRequest( + request = payRequest, + walletServicePubkey = nwcConfig.pubKeyHex, + signer = clientSigner, +) + +// Create an invoice (amount in millisats) +val invoiceRequest = MakeInvoiceMethod.create( + amount = 50000L, // 50 sats in millisats + description = "Coffee", +) +val invoiceEvent = LnZapPaymentRequestEvent.createRequest( + request = invoiceRequest, + walletServicePubkey = nwcConfig.pubKeyHex, + signer = clientSigner, +) + +// List transactions with pagination +val listRequest = ListTransactionsMethod.create( + limit = 20, + offset = 0, +) +val listEvent = LnZapPaymentRequestEvent.createRequest( + request = listRequest, + walletServicePubkey = nwcConfig.pubKeyHex, + signer = clientSigner, +) +``` + +To use NIP-44 encryption instead of NIP-04: + +```kotlin +val event = LnZapPaymentRequestEvent.createRequest( + request = GetInfoMethod.create(), + walletServicePubkey = nwcConfig.pubKeyHex, + signer = clientSigner, + useNip44 = true, +) +``` + +### 4. Receive and Parse Responses + +Subscribe to kind `23195` events on the NWC relay, filtered by the wallet +service pubkey and the request event ID. When a response arrives: + +```kotlin +// responseEvent is a LnZapPaymentResponseEvent (kind 23195) +val response: Response = responseEvent.decrypt(clientSigner) + +when (response) { + is GetBalanceSuccessResponse -> { + val balanceMillisats = response.result?.balance ?: 0L + val balanceSats = balanceMillisats / 1000L + } + is PayInvoiceSuccessResponse -> { + val preimage = response.result?.preimage + val feesPaid = response.result?.fees_paid + } + is MakeInvoiceSuccessResponse -> { + val bolt11 = response.result?.invoice + val paymentHash = response.result?.payment_hash + } + is ListTransactionsSuccessResponse -> { + val transactions: List = response.result?.transactions ?: emptyList() + transactions.forEach { tx -> + // tx.type — "incoming" or "outgoing" + // tx.amount — in millisats + // tx.description, tx.created_at, tx.state, etc. + } + } + is GetInfoSuccessResponse -> { + val alias = response.result?.alias + val methods = response.result?.methods // supported methods + val lud16 = response.result?.lud16 + } + is PayInvoiceErrorResponse -> { + val errorCode = response.error?.code // NwcErrorCode enum + val errorMessage = response.error?.message + } + is NwcErrorResponse -> { + val errorCode = response.error?.code + val errorMessage = response.error?.message + } +} +``` + +### 5. Listen for Notifications (Optional) + +Subscribe to kind `23196`/`23197` events from the wallet: + +```kotlin +// notificationEvent is an NwcNotificationEvent +val notification: Notification = notificationEvent.decryptNotification(clientSigner) + +when (notification) { + is PaymentReceivedNotification -> { + val tx: NwcTransaction? = notification.notification + // tx?.amount, tx?.description, tx?.payment_hash, etc. + } + is PaymentSentNotification -> { + val tx: NwcTransaction? = notification.notification + } + is HoldInvoiceAcceptedNotification -> { + val data = notification.notification + // data?.payment_hash, data?.amount, data?.settle_deadline + } +} +``` + +### 6. Transaction State Helpers + +Transaction states from different wallet implementations may use different +casing. Use the case-insensitive helpers: + +```kotlin +val tx: NwcTransaction = ... + +NwcTransactionState.isSettled(tx.state) // true for "SETTLED" or "settled" +NwcTransactionState.isPending(tx.state) // true for "PENDING" or "pending" +NwcTransactionState.isFailed(tx.state) // true for "FAILED" or "failed" +NwcTransactionState.isAccepted(tx.state) // true for "ACCEPTED" or "accepted" +``` + +### 7. URI Persistence + +To save/restore the NWC connection: + +```kotlin +// Save +val nip47URI: Nip47WalletConnect.Nip47URI = nwcConfig.denormalize()!! +val json = Nip47WalletConnect.Nip47URI.serializer(nip47URI) + +// Restore +val restored = Nip47WalletConnect.Nip47URI.parser(json) +val normalized = restored.normalize()!! +``` + +## Implementing a Wallet Service + +A wallet service receives NWC requests from clients, processes them (e.g., +pays invoices via a Lightning node), and sends back responses. + +### 1. Publish Capabilities + +Advertise which methods your wallet supports by publishing a kind `13194` +event: + +```kotlin +val capabilities = listOf( + NwcMethod.PAY_INVOICE, + NwcMethod.GET_BALANCE, + NwcMethod.GET_INFO, + NwcMethod.MAKE_INVOICE, + NwcMethod.LOOKUP_INVOICE, + NwcMethod.LIST_TRANSACTIONS, +) + +val infoTemplate = NwcInfoEvent.build( + capabilities = capabilities, + encryptionSchemes = listOf("nip04", "nip44_v2"), + notificationTypes = listOf( + NwcNotificationType.PAYMENT_RECEIVED, + NwcNotificationType.PAYMENT_SENT, + ), +) +// Sign with wallet signer: walletSigner.sign(infoTemplate) +``` + +### 2. Receive and Parse Requests + +Subscribe to kind `23194` events on your relay, filtered by your wallet +service pubkey in the `p` tag. When a request arrives: + +```kotlin +// requestEvent is a LnZapPaymentRequestEvent (kind 23194) +val request: Request = requestEvent.decryptRequest(walletSigner) + +when (request) { + is PayInvoiceMethod -> { + val bolt11 = request.params?.invoice + val amount = request.params?.amount // optional override in millisats + // Process payment via your Lightning node... + } + is GetBalanceMethod -> { + // Query your Lightning node for balance... + } + is MakeInvoiceMethod -> { + val amount = request.params?.amount // in millisats + val description = request.params?.description + // Create invoice via your Lightning node... + } + is ListTransactionsMethod -> { + val limit = request.params?.limit + val offset = request.params?.offset + val type = request.params?.type // "incoming" or "outgoing" + // Query transaction history... + } + is GetInfoMethod -> { + // Return node info... + } + is GetBudgetMethod -> { + // Return budget info... + } + // ... handle other methods +} +``` + +### 3. Build and Send Responses + +Use `LnZapPaymentResponseEvent.createResponse()` to create encrypted +response events: + +```kotlin +// Success response for get_balance +val balanceResponse = GetBalanceSuccessResponse( + GetBalanceSuccessResponse.GetBalanceResult(balance = 2100000L) // in millisats +) +val responseEvent = LnZapPaymentResponseEvent.createResponse( + response = balanceResponse, + requestEvent = requestEvent, + signer = walletSigner, +) +// Send responseEvent to the relay + +// Success response for pay_invoice +val payResponse = PayInvoiceSuccessResponse( + PayInvoiceSuccessResponse.PayInvoiceResultParams( + preimage = "0123456789abcdef", + fees_paid = 100L, + ) +) +val payResponseEvent = LnZapPaymentResponseEvent.createResponse( + response = payResponse, + requestEvent = requestEvent, + signer = walletSigner, +) + +// Error response +val errorResponse = NwcErrorResponse( + resultType = NwcMethod.PAY_INVOICE, + error = NwcError(NwcErrorCode.INSUFFICIENT_BALANCE, "Not enough funds"), +) +val errorEvent = LnZapPaymentResponseEvent.createResponse( + response = errorResponse, + requestEvent = requestEvent, + signer = walletSigner, +) +``` + +To use NIP-44 encryption for responses: + +```kotlin +val responseEvent = LnZapPaymentResponseEvent.createResponse( + response = balanceResponse, + requestEvent = requestEvent, + signer = walletSigner, + useNip44 = true, +) +``` + +### 4. Send Notifications + +Push notifications to clients for payment events: + +```kotlin +// Payment received notification +val notification = PaymentReceivedNotification( + notification = NwcTransaction( + type = NwcTransactionType.INCOMING, + state = NwcTransactionState.SETTLED, + invoice = "lnbc...", + amount = 50000L, // in millisats + payment_hash = "abc123", + settled_at = TimeUtils.now(), + created_at = TimeUtils.now(), + ), +) +val notifEvent = NwcNotificationEvent.createNotification( + notification = notification, + clientPubkey = clientPubkeyHex, + signer = walletSigner, +) +// Send notifEvent to the relay +``` + +### 5. Build Transaction Objects + +Transactions are used across multiple response types: + +```kotlin +val transaction = NwcTransaction( + type = NwcTransactionType.INCOMING, // or OUTGOING + state = NwcTransactionState.SETTLED, // PENDING, SETTLED, FAILED, ACCEPTED + invoice = "lnbc50n1...", + description = "Coffee payment", + payment_hash = "abc123def456", + preimage = "fedcba654321", + amount = 50000L, // in millisats + fees_paid = 100L, // in millisats + created_at = 1693876497L, // unix timestamp + settled_at = 1693876500L, + expires_at = 1694876497L, +) +``` + +## Error Codes + +| Code | When to Use | +|-------------------------|-------------------------------------------| +| `RATE_LIMITED` | Too many requests | +| `NOT_IMPLEMENTED` | Method not supported by wallet | +| `INSUFFICIENT_BALANCE` | Not enough funds for payment | +| `PAYMENT_FAILED` | Payment could not be completed | +| `QUOTA_EXCEEDED` | Budget/spending limit exceeded | +| `RESTRICTED` | Method not allowed for this connection | +| `UNAUTHORIZED` | Invalid or expired credentials | +| `INTERNAL` | Internal wallet error | +| `UNSUPPORTED_ENCRYPTION`| Requested encryption not supported | +| `BAD_REQUEST` | Malformed request parameters | +| `NOT_FOUND` | Invoice or resource not found | +| `EXPIRED` | Connection or invoice expired | +| `OTHER` | Unspecified error | + +## Encryption + +NWC supports two encryption schemes: + +- **NIP-04** (default): Set `useNip44 = false` in event builders +- **NIP-44 v2**: Set `useNip44 = true` in event builders + +When building requests with NIP-44, the event includes an `encryption` tag: +``` +["encryption", "nip44_v2"] +``` + +Clients can check a wallet's supported encryption via the info event: +```kotlin +val infoEvent: NwcInfoEvent = ... +val schemes: List = infoEvent.encryptionSchemes() +// e.g., ["nip04", "nip44_v2"] +``` + +## Caching + +For apps handling many concurrent NWC events, use the built-in LRU caches: + +```kotlin +val requestCache = NostrWalletConnectRequestCache(signer) +val responseCache = NostrWalletConnectResponseCache(signer) + +// These cache up to 50 decrypted results and handle +// async retry logic for permission dialogs and timeouts. +val request: Request? = requestCache.decryptRequest(requestEvent) +val response: Response? = responseCache.decryptResponse(responseEvent) +``` + +## Amounts + +All amounts in NWC are in **millisatoshis** (1 sat = 1000 msats). Convert for +display: + +```kotlin +val balanceMsats = response.result?.balance ?: 0L +val balanceSats = balanceMsats / 1000L +``` + +## Interoperability + +This implementation is tested against: +- **Alby Hub** (server) — uppercase transaction states, all error codes +- **Alby JS SDK** (client) — lowercase transaction states, budget renewal + periods, structured metadata + +See `AlbyInteropTest.kt` for real-world test vectors. From 3f1e826a10d1209fae6befb15fa700090fbd6393 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 06:00:21 +0100 Subject: [PATCH 074/172] update cz, pt, de, sv --- .../src/main/res/values-cs-rCZ/strings.xml | 27 +++++++++++++++++++ .../src/main/res/values-de-rDE/strings.xml | 27 +++++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 27 +++++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 27 +++++++++++++++++++ 4 files changed, 108 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index bb5983ec04..b87167d443 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1503,4 +1503,31 @@ Vybrat vše %1$d%% dostupnost Nastavení Namecoin + Přidat datum vypršení + Odebrat datum vypršení + Datum vypršení + Klienti příspěvek po tomto datu skryjí (NIP-40) + Vyberte datum a čas vypršení + Vyprší za %1$s + Čas vypršení + Exportovat nastavení relé + Exportovat jako text + Exportovat jako ZIP (JSON) + Hledat jazyky + Přidat jazyk + Přidat jazykový pár + Zdrojový jazyk + Cílový jazyk + Zobrazit %1$s jako první + Zatím žádné předvolby zobrazení jazyka. Vytvoří se automaticky při překladu nebo je můžete přidat ručně. + Smazat předvolbu + Tato zpráva zmizí za %1$s + Průzkumník Bitcoin (OTS) + Nelze otevřít Blossom odkazy + Nebyly nalezeny žádné aplikace Blossom. Nainstalujte prosím lokální aplikaci Blossom pro zobrazení tohoto souboru + Přeložit do + Vyberte jazyk, do kterého chcete obsah přeložit. + Předvolby zobrazení jazyka + Pro každý přeložený jazykový pár zvolte, který jazyk zobrazit jako první. + %1$s → %2$s diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 2684746a0b..5d5e61c991 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1508,4 +1508,31 @@ anz der Bedingungen ist erforderlich Alle auswählen %1$d%% Verfügbarkeit Namecoin-Einstellungen + Ablaufdatum hinzufügen + Ablaufdatum entfernen + Ablaufdatum + Der Beitrag wird von Clients nach diesem Datum ausgeblendet (NIP-40) + Ablaufdatum und -uhrzeit auswählen + Läuft ab in %1$s + Ablaufzeit + Relay-Einstellungen exportieren + Als Text exportieren + Als ZIP exportieren (JSON) + Sprachen suchen + Sprache hinzufügen + Sprachpaar hinzufügen + Ausgangssprache + Zielsprache + %1$s zuerst anzeigen + Noch keine Sprachanzeigeeinstellungen. Diese werden automatisch bei Übersetzungen erstellt oder können manuell hinzugefügt werden. + Einstellung löschen + Diese Nachricht verschwindet in %1$s + Bitcoin Explorer (OTS) + Blossom-Links können nicht geöffnet werden + Keine Blossom-Apps gefunden. Bitte installiere eine lokale Blossom-App, um diese Datei anzuzeigen + Übersetzen in + Wähle die Sprache, in die der Inhalt übersetzt werden soll. + Sprachanzeigeeinstellungen + Wähle für jedes übersetzte Sprachpaar, welche Sprache zuerst angezeigt werden soll. + %1$s → %2$s diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 7a20de62c0..9ee71b9328 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1503,4 +1503,31 @@ Selecionar tudo %1$d%% de disponibilidade Configurações do Namecoin + Adicionar data de expiração + Remover data de expiração + Data de expiração + A publicação será ocultada pelos clientes após esta data (NIP-40) + Selecionar data e hora de expiração + Expira em %1$s + Hora de expiração + Exportar configurações de relay + Exportar como texto + Exportar como ZIP (JSON) + Buscar idiomas + Adicionar idioma + Adicionar par de idiomas + Idioma de origem + Idioma de destino + Mostrar %1$s primeiro + Nenhuma preferência de exibição de idioma ainda. Elas são criadas automaticamente quando ocorrem traduções, ou você pode adicioná-las manualmente. + Excluir preferência + Esta mensagem desaparecerá em %1$s + Explorador Bitcoin (OTS) + Não é possível abrir links Blossom + Nenhum aplicativo Blossom foi encontrado. Instale um aplicativo Blossom local para visualizar este arquivo + Traduzir para + Escolha o idioma para o qual o conteúdo será traduzido. + Preferências de exibição de idioma + Para cada par de idiomas traduzido, escolha qual idioma exibir primeiro. + %1$s → %2$s diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 64f7432515..b1dd817cc9 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1502,4 +1502,31 @@ Välj alla %1$d%% drifttid Namecoin-inställningar + Lägg till utgångsdatum + Ta bort utgångsdatum + Utgångsdatum + Inlägget döljs av klienter efter detta datum (NIP-40) + Välj utgångsdatum och utgångstid + Går ut om %1$s + Utgångstid + Exportera relay-inställningar + Exportera som text + Exportera som ZIP (JSON) + Sök språk + Lägg till språk + Lägg till språkpar + Källspråk + Målspråk + Visa %1$s först + Inga språkvisningsinställningar än. Dessa skapas automatiskt när översättningar sker, eller kan läggas till manuellt. + Ta bort inställning + Detta meddelande försvinner om %1$s + Bitcoin Explorer (OTS) + Kan inte öppna Blossom-länkar + Inga Blossom-appar hittades. Installera en lokal Blossom-app för att visa den här filen + Översätt till + Välj det språk du vill översätta innehållet till. + Språkvisningsinställningar + Välj för varje översatt språkpar vilket språk som ska visas först. + %1$s → %2$s From f9f4e7716d6fc299a73839a08b3049b1e6c20d9f Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 14 Mar 2026 05:06:26 +0000 Subject: [PATCH 075/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 51 +++++++++--------- .../src/main/res/values-de-rDE/strings.xml | 52 +++++++++---------- .../src/main/res/values-pt-rBR/strings.xml | 51 +++++++++--------- .../src/main/res/values-sv-rSE/strings.xml | 52 +++++++++---------- 4 files changed, 100 insertions(+), 106 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index b87167d443..e12ec2833a 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -923,6 +923,8 @@ Ujistěte se, že podepisující aplikace autorizovala tuto transakci Nebyly nalezeny žádné peněženky pro platbu bleskové faktury (Chyba: %1$s). Prosím, nainstalujte si bleskovou peněženku pro použití Zapů Nebyly nalezeny žádné peněženky pro platbu bleskové faktury. Prosím, nainstalujte si bleskovou peněženku pro použití Zapů + Nelze otevřít Blossom odkazy + Nebyly nalezeny žádné aplikace Blossom. Nainstalujte prosím lokální aplikaci Blossom pro zobrazení tohoto souboru Skrytá slova Skrýt nové slovo nebo větu Profilový obrázek @@ -1092,6 +1094,13 @@ Zrušit rozdělení Zap Přidat upozornění na obsah Odstranit upozornění na obsah + Přidat datum vypršení + Odebrat datum vypršení + Datum vypršení + Klienti příspěvek po tomto datu skryjí (NIP-40) + Vyberte datum a čas vypršení + Vyprší za %1$s + Čas vypršení Zobrazit npub jako QR kód Zobrazit nprofile jako QR kód Neplatná adresa @@ -1161,6 +1170,9 @@ Blokované Relé Blokované Relé Amethyst se k těmto relé nikdy nepřipojí + Exportovat nastavení relé + Exportovat jako text + Exportovat jako ZIP (JSON) Zapni vývojáře! Váš příspěvek nám pomáhá dělat rozdíl. Každý sat se počítá! Přispět nyní @@ -1266,6 +1278,18 @@ Vyhledávání hashtag: #%1$s Nepřekládat z Zde zobrazené jazyky nebudou přeloženy. Vyberte jazyk, který chcete odstranit a nechat je znovu přeložit. + Přeložit do + Vyberte jazyk, do kterého chcete obsah přeložit. + Předvolby zobrazení jazyka + Pro každý přeložený jazykový pár zvolte, který jazyk zobrazit jako první. + Hledat jazyky + Přidat jazyk + Přidat jazykový pár + Zdrojový jazyk + Cílový jazyk + Zobrazit %1$s jako první + Zatím žádné předvolby zobrazení jazyka. Vytvoří se automaticky při překladu nebo je můžete přidat ručně. + Smazat předvolbu Pozastavit Hrát Otevřít rozbalovací nabídku @@ -1275,6 +1299,7 @@ Nalezen záznam o pádu Chcete poslat poslední záznam o pádu do Amethystu v soukromé zprávě? Žádné osobní údaje nebudou sdíleny Odeslat + Tato zpráva zmizí za %1$s Tato zpráva zmizí za %1$d dní Vybrat podepisovatele Již v seznamu @@ -1503,31 +1528,5 @@ Vybrat vše %1$d%% dostupnost Nastavení Namecoin - Přidat datum vypršení - Odebrat datum vypršení - Datum vypršení - Klienti příspěvek po tomto datu skryjí (NIP-40) - Vyberte datum a čas vypršení - Vyprší za %1$s - Čas vypršení - Exportovat nastavení relé - Exportovat jako text - Exportovat jako ZIP (JSON) - Hledat jazyky - Přidat jazyk - Přidat jazykový pár - Zdrojový jazyk - Cílový jazyk - Zobrazit %1$s jako první - Zatím žádné předvolby zobrazení jazyka. Vytvoří se automaticky při překladu nebo je můžete přidat ručně. - Smazat předvolbu - Tato zpráva zmizí za %1$s Průzkumník Bitcoin (OTS) - Nelze otevřít Blossom odkazy - Nebyly nalezeny žádné aplikace Blossom. Nainstalujte prosím lokální aplikaci Blossom pro zobrazení tohoto souboru - Přeložit do - Vyberte jazyk, do kterého chcete obsah přeložit. - Předvolby zobrazení jazyka - Pro každý přeložený jazykový pár zvolte, který jazyk zobrazit jako první. - %1$s → %2$s diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 5d5e61c991..a1df34e69a 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -928,6 +928,8 @@ anz der Bedingungen ist erforderlich Stellen Sie sicher, dass die Unterzeichner-Anwendung diese Transaktion autorisiert hat Keine Wallets gefunden, um eine Lightning-Rechnung zu bezahlen (Fehler: %1$s). Installieren Sie eine Lightning-Wallet, um Zaps zu verwenden Keine Wallets gefunden, um eine Lightning-Rechnung zu bezahlen. Installieren Sie eine Lightning-Wallet, um Zaps zu verwenden + Blossom-Links können nicht geöffnet werden + Keine Blossom-Apps gefunden. Bitte installiere eine lokale Blossom-App, um diese Datei anzuzeigen Versteckte Wörter Neues Wort oder neuen Satz verstecken Profilbild @@ -1097,6 +1099,13 @@ anz der Bedingungen ist erforderlich Zap-Aufteilung abbrechen Inhaltswarnung hinzufügen Inhaltswarnung entfernen + Ablaufdatum hinzufügen + Ablaufdatum entfernen + Ablaufdatum + Der Beitrag wird von Clients nach diesem Datum ausgeblendet (NIP-40) + Ablaufdatum und -uhrzeit auswählen + Läuft ab in %1$s + Ablaufzeit Npub als QR-Code anzeigen nprofile als QR-Code anzeigen Ungültige Adresse @@ -1166,6 +1175,9 @@ anz der Bedingungen ist erforderlich Blockierte Relays Blockierte Relays Amethyst wird sich niemals mit diesen Relays verbinden + Relay-Einstellungen exportieren + Als Text exportieren + Als ZIP exportieren (JSON) Zap die Entwickler! Deine Spende hilft uns, einen Unterschied zu machen. Jeder Sat zählt! Jetzt spenden @@ -1271,6 +1283,18 @@ anz der Bedingungen ist erforderlich Suche Hashtag: #%1$s Nicht übersetzen von Die hier angezeigten Sprachen werden nicht übersetzt. Wählen Sie eine Sprache, um sie zu entfernen und lassen Sie sie erneut übersetzen. + Übersetzen in + Wähle die Sprache, in die der Inhalt übersetzt werden soll. + Sprachanzeigeeinstellungen + Wähle für jedes übersetzte Sprachpaar, welche Sprache zuerst angezeigt werden soll. + Sprachen suchen + Sprache hinzufügen + Sprachpaar hinzufügen + Ausgangssprache + Zielsprache + %1$s zuerst anzeigen + Noch keine Sprachanzeigeeinstellungen. Diese werden automatisch bei Übersetzungen erstellt oder können manuell hinzugefügt werden. + Einstellung löschen Pausen Abspielen Dropdown-Menü öffnen @@ -1280,6 +1304,7 @@ anz der Bedingungen ist erforderlich Absturzbericht gefunden Möchten Sie den letzten Absturzbericht per Direktnachricht an Amethyst senden? Es werden keine persönlichen Daten weitergegeben Senden + Diese Nachricht verschwindet in %1$s Diese Nachricht verschwindet in %1$d Tagen Signierer auswählen Bereits in der Liste @@ -1508,31 +1533,4 @@ anz der Bedingungen ist erforderlich Alle auswählen %1$d%% Verfügbarkeit Namecoin-Einstellungen - Ablaufdatum hinzufügen - Ablaufdatum entfernen - Ablaufdatum - Der Beitrag wird von Clients nach diesem Datum ausgeblendet (NIP-40) - Ablaufdatum und -uhrzeit auswählen - Läuft ab in %1$s - Ablaufzeit - Relay-Einstellungen exportieren - Als Text exportieren - Als ZIP exportieren (JSON) - Sprachen suchen - Sprache hinzufügen - Sprachpaar hinzufügen - Ausgangssprache - Zielsprache - %1$s zuerst anzeigen - Noch keine Sprachanzeigeeinstellungen. Diese werden automatisch bei Übersetzungen erstellt oder können manuell hinzugefügt werden. - Einstellung löschen - Diese Nachricht verschwindet in %1$s - Bitcoin Explorer (OTS) - Blossom-Links können nicht geöffnet werden - Keine Blossom-Apps gefunden. Bitte installiere eine lokale Blossom-App, um diese Datei anzuzeigen - Übersetzen in - Wähle die Sprache, in die der Inhalt übersetzt werden soll. - Sprachanzeigeeinstellungen - Wähle für jedes übersetzte Sprachpaar, welche Sprache zuerst angezeigt werden soll. - %1$s → %2$s diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 9ee71b9328..4ab3d5decb 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -923,6 +923,8 @@ Certifique-se de que a aplicação assinante autorizou esta transação Nenhuma carteira encontrada para pagar uma fatura Lightning (Erro: %1$s). Instale uma carteira Lightning para usar zaps Nenhuma carteira encontrada para pagar uma fatura Lightning. Instale uma carteira Lightning para usar zaps + Não é possível abrir links Blossom + Nenhum aplicativo Blossom foi encontrado. Instale um aplicativo Blossom local para visualizar este arquivo Palavras Ocultas Ocultar nova palavra ou frase Foto de Perfil @@ -1092,6 +1094,13 @@ Cancelar Divisão Zap Adicionar aviso de conteúdo Remover aviso de conteúdo + Adicionar data de expiração + Remover data de expiração + Data de expiração + A publicação será ocultada pelos clientes após esta data (NIP-40) + Selecionar data e hora de expiração + Expira em %1$s + Hora de expiração Mostrar npub como um código QR Mostrar nprofile como QR code Endereço inválido @@ -1161,6 +1170,9 @@ Relays bloqueados Relays bloqueados O Amethyst nunca se conectará a esses relays + Exportar configurações de relay + Exportar como texto + Exportar como ZIP (JSON) Zap os desenvolvedores! Sua doação nos ajuda a fazer a diferença. Cada sat conta! Doar agora @@ -1266,6 +1278,18 @@ Pesquisar hashtag: #%1$s Não Traduzir de Os idiomas mostrados aqui não serão traduzidos. Selecione um idioma para removê-lo e traduzi-lo novamente. + Traduzir para + Escolha o idioma para o qual o conteúdo será traduzido. + Preferências de exibição de idioma + Para cada par de idiomas traduzido, escolha qual idioma exibir primeiro. + Buscar idiomas + Adicionar idioma + Adicionar par de idiomas + Idioma de origem + Idioma de destino + Mostrar %1$s primeiro + Nenhuma preferência de exibição de idioma ainda. Elas são criadas automaticamente quando ocorrem traduções, ou você pode adicioná-las manualmente. + Excluir preferência Pausar Reproduzir Abrir menu suspenso @@ -1275,6 +1299,7 @@ Relatório de falha encontrado Gostaria de enviar o relatório de falha recente para o Amethyst em uma DM? Nenhuma informação pessoal será compartilhada Enviar + Esta mensagem desaparecerá em %1$s Esta mensagem desaparecerá em %1$d dias Selecionar assinador Já está na lista @@ -1503,31 +1528,5 @@ Selecionar tudo %1$d%% de disponibilidade Configurações do Namecoin - Adicionar data de expiração - Remover data de expiração - Data de expiração - A publicação será ocultada pelos clientes após esta data (NIP-40) - Selecionar data e hora de expiração - Expira em %1$s - Hora de expiração - Exportar configurações de relay - Exportar como texto - Exportar como ZIP (JSON) - Buscar idiomas - Adicionar idioma - Adicionar par de idiomas - Idioma de origem - Idioma de destino - Mostrar %1$s primeiro - Nenhuma preferência de exibição de idioma ainda. Elas são criadas automaticamente quando ocorrem traduções, ou você pode adicioná-las manualmente. - Excluir preferência - Esta mensagem desaparecerá em %1$s Explorador Bitcoin (OTS) - Não é possível abrir links Blossom - Nenhum aplicativo Blossom foi encontrado. Instale um aplicativo Blossom local para visualizar este arquivo - Traduzir para - Escolha o idioma para o qual o conteúdo será traduzido. - Preferências de exibição de idioma - Para cada par de idiomas traduzido, escolha qual idioma exibir primeiro. - %1$s → %2$s diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index b1dd817cc9..60a129aa57 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -922,6 +922,8 @@ Kontrollera att signeringsprogrammet har godkänt denna transaktion Inga plånböcker hittades för att betala en blixtfaktura (Fel: %1$s). Installera en blixtpengaplånbok för att använda zaps Inga plånböcker hittades för att betala en blixtfaktura. Installera en blixtpengaplånbok för att använda zaps + Kan inte öppna Blossom-länkar + Inga Blossom-appar hittades. Installera en lokal Blossom-app för att visa den här filen Dolda ord Dölj nytt ord eller mening Profilbild @@ -1091,6 +1093,13 @@ Avbryt Zap-split Lägg till varning för innehåll Ta bort varning för innehåll + Lägg till utgångsdatum + Ta bort utgångsdatum + Utgångsdatum + Inlägget döljs av klienter efter detta datum (NIP-40) + Välj utgångsdatum och utgångstid + Går ut om %1$s + Utgångstid Visa npub som en QR-kod Visa nprofile som QR-kod Ogiltig adress @@ -1160,6 +1169,9 @@ Blockerade reläer Blockerade reläer Amethyst kommer aldrig att ansluta till dessa reläer + Exportera relay-inställningar + Exportera som text + Exportera som ZIP (JSON) Zappa utvecklarna! Din donation hjälper oss att göra skillnad. Varje sat räknas! Donera nu @@ -1265,6 +1277,18 @@ Sök hashtag: #%1$s Översätt inte från Språk som visas här kommer inte att översättas. Välj ett språk för att ta bort det och få det översatt igen. + Översätt till + Välj det språk du vill översätta innehållet till. + Språkvisningsinställningar + Välj för varje översatt språkpar vilket språk som ska visas först. + Sök språk + Lägg till språk + Lägg till språkpar + Källspråk + Målspråk + Visa %1$s först + Inga språkvisningsinställningar än. Dessa skapas automatiskt när översättningar sker, eller kan läggas till manuellt. + Ta bort inställning Pausa Spela Öppna rullgardinsmeny @@ -1274,6 +1298,7 @@ Kraschrapport hittad Vill du skicka den senaste kraschrapporten till Amethyst i ett DM? Ingen personlig information kommer att delas Skicka + Detta meddelande försvinner om %1$s Detta meddelande försvinner om %1$d dagar Välj signatör Redan i listan @@ -1502,31 +1527,4 @@ Välj alla %1$d%% drifttid Namecoin-inställningar - Lägg till utgångsdatum - Ta bort utgångsdatum - Utgångsdatum - Inlägget döljs av klienter efter detta datum (NIP-40) - Välj utgångsdatum och utgångstid - Går ut om %1$s - Utgångstid - Exportera relay-inställningar - Exportera som text - Exportera som ZIP (JSON) - Sök språk - Lägg till språk - Lägg till språkpar - Källspråk - Målspråk - Visa %1$s först - Inga språkvisningsinställningar än. Dessa skapas automatiskt när översättningar sker, eller kan läggas till manuellt. - Ta bort inställning - Detta meddelande försvinner om %1$s - Bitcoin Explorer (OTS) - Kan inte öppna Blossom-länkar - Inga Blossom-appar hittades. Installera en lokal Blossom-app för att visa den här filen - Översätt till - Välj det språk du vill översätta innehållet till. - Språkvisningsinställningar - Välj för varje översatt språkpar vilket språk som ska visas först. - %1$s → %2$s From a6d5944bf32bf288b1914540317f6cbd274622c5 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 14 Mar 2026 09:01:55 +0000 Subject: [PATCH 076/172] Fix JSON formatting in settings.json --- .claude/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index af65d2dbca..80496cbbab 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -9,7 +9,7 @@ } ] } - ] + ], "Stop": [ { "matcher": "", @@ -23,4 +23,4 @@ } ] } -} \ No newline at end of file +} From dcf1e4e39b9e57740a330e80d36ad166cada0e42 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 14 Mar 2026 11:08:34 +0000 Subject: [PATCH 077/172] Add Claude Code local settings to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 43404b2597..d87f7f1462 100644 --- a/.gitignore +++ b/.gitignore @@ -149,3 +149,6 @@ lint/tmp/ # Local task tracking TASKS.md + +# Claude Code local settings +.claude/settings.local.json From 4fc97f21c5e03ba7350fc9127c700e619de840f7 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 14 Mar 2026 11:10:01 +0000 Subject: [PATCH 078/172] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hu-rHU/strings.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index f385c1347e..cdaa9bb882 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -1099,6 +1099,13 @@ Zap-megosztások visszavonása Tartalmi figyelmeztetés hozzáadása Tartalmi figyelmeztetés eltávolítása + Lejárati dátum hozzáadása + Lejárati dátum törlése + Lejárati dátum + Ezen dátum után a klienek elrejtik a bejegyzést (NIP-40) + Válassza ki a lejárati dátumot és időpontot + Lejár ekkor: %1$s + Lejárati idő Az npub-kulcs megjelenítése QR-kódként nprofile-kulcs megjelenítése QR-kódként Érvénytelen cím @@ -1168,6 +1175,9 @@ Letiltott átjátszók Letiltott átjátszók Az Amethyst soha nem fog csatlakozni ezekhez az átjátszókhoz + Átjátszóbeállítások exportálása + Exportálás szövegként + Exportálás ZIP-fájlként (JSON) Zap a fejlesztőknek! Az Ön adománya segít nekünk abban, hogy változtassunk a dolgokon. Minden satoshi számít! Adományozás most @@ -1295,6 +1305,7 @@ Összeomlási jelentés megtalálva Szeretné elküldeni a legutóbbi összeomlási jelentést az Amethystnek egy közvetlen üzenetben? A személyes adatait nem osztja meg Küldés + Ez az üzenet %1$s után eltűnik Ez az üzenet %1$d nap múlva eltűnik Aláíró kiválasztása Már rajta van a listán @@ -1523,4 +1534,5 @@ Összes kijelölése Üzemidő: %1$d%% Namecoin-beállítások + Bitcoin felfedező (OTS) From 1841e47c5ccd6a5728dc0bbb706f77d2e934b699 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 11:39:17 +0000 Subject: [PATCH 079/172] feat: add high-level Nip47Client and Nip47Server APIs for NIP-47 Simplifies the NWC developer experience by providing Nip47Client (for wallet client apps) and Nip47Server (for wallet service backends) that handle URI parsing, signer creation, event building, filter construction, and response decryption. Updates README with quick-start examples. https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../quartz/nip47WalletConnect/Nip47Client.kt | 265 ++++++++++++ .../quartz/nip47WalletConnect/Nip47Server.kt | 294 +++++++++++++ .../quartz/nip47WalletConnect/README.md | 398 ++++++------------ 3 files changed, 699 insertions(+), 258 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt new file mode 100644 index 0000000000..5686fc1d53 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal + +/** + * High-level NIP-47 Wallet Connect client. + * + * Simplifies the NWC protocol by handling URI parsing, signer creation, + * event building, filter construction, and response decryption. + * + * Usage: + * ```kotlin + * val client = Nip47Client.fromUri("nostr+walletconnect://pubkey?relay=...&secret=...") + * + * // Build a request event + * val requestEvent = client.payInvoice("lnbc50n1...") + * + * // Send requestEvent to client.relayUrl via your relay connection + * // Subscribe using client.responseFilter(requestEvent.id) for the response + * + * // When response arrives: + * val response = client.parseResponse(responseEvent) + * when (response) { + * is PayInvoiceSuccessResponse -> println("Paid! Preimage: ${response.result?.preimage}") + * is NwcErrorResponse -> println("Error: ${response.error?.message}") + * } + * ``` + */ +class Nip47Client( + val walletPubKeyHex: HexKey, + val relayUrl: NormalizedRelayUrl, + val signer: NostrSigner, + val useNip44: Boolean = false, +) { + companion object { + /** + * Creates an Nip47Client from a NWC connection URI string. + * + * @param uri NWC URI (e.g., "nostr+walletconnect://pubkey?relay=...&secret=...") + * @throws IllegalArgumentException if the URI is invalid or has no secret + */ + fun fromUri(uri: String): Nip47Client { + val config = Nip47WalletConnect.parse(uri) + return fromNip47URI(config) + } + + /** + * Creates an Nip47Client from parsed NWC connection details. + * + * @param config parsed NWC URI with wallet pubkey, relay, and secret + * @throws IllegalArgumentException if config has no secret + */ + fun fromNip47URI(config: Nip47WalletConnect.Nip47URINorm): Nip47Client { + val secret = config.secret ?: throw IllegalArgumentException("NWC connection requires a secret") + val signer = NostrSignerInternal(KeyPair(secret.hexToByteArray())) + return Nip47Client( + walletPubKeyHex = config.pubKeyHex, + relayUrl = config.relayUri, + signer = signer, + ) + } + } + + // --- Request builders --- + + /** + * Builds a pay_invoice request event. + */ + suspend fun payInvoice( + bolt11: String, + amount: Long? = null, + ): LnZapPaymentRequestEvent = + buildRequest( + if (amount != null) { + PayInvoiceMethod.create(bolt11, amount) + } else { + PayInvoiceMethod.create(bolt11) + }, + ) + + /** + * Builds a pay_keysend request event. + */ + suspend fun payKeysend( + amount: Long, + pubkey: String, + preimage: String? = null, + tlvRecords: List? = null, + ): LnZapPaymentRequestEvent = buildRequest(PayKeysendMethod.create(amount, pubkey, preimage, tlvRecords)) + + /** + * Builds a get_balance request event. + */ + suspend fun getBalance(): LnZapPaymentRequestEvent = buildRequest(GetBalanceMethod.create()) + + /** + * Builds a get_info request event. + */ + suspend fun getInfo(): LnZapPaymentRequestEvent = buildRequest(GetInfoMethod.create()) + + /** + * Builds a make_invoice request event. + */ + suspend fun makeInvoice( + amount: Long, + description: String? = null, + descriptionHash: String? = null, + expiry: Long? = null, + ): LnZapPaymentRequestEvent = buildRequest(MakeInvoiceMethod.create(amount, description, descriptionHash, expiry)) + + /** + * Builds a lookup_invoice request event by payment hash. + */ + suspend fun lookupInvoiceByHash(paymentHash: String): LnZapPaymentRequestEvent = + buildRequest(LookupInvoiceMethod.createByHash(paymentHash)) + + /** + * Builds a lookup_invoice request event by BOLT11 invoice. + */ + suspend fun lookupInvoiceByInvoice(invoice: String): LnZapPaymentRequestEvent = + buildRequest(LookupInvoiceMethod.createByInvoice(invoice)) + + /** + * Builds a list_transactions request event. + */ + suspend fun listTransactions( + from: Long? = null, + until: Long? = null, + limit: Int? = null, + offset: Int? = null, + unpaid: Boolean? = null, + type: String? = null, + ): LnZapPaymentRequestEvent = buildRequest(ListTransactionsMethod.create(from, until, limit, offset, unpaid, type)) + + /** + * Builds a get_budget request event. + */ + suspend fun getBudget(): LnZapPaymentRequestEvent = buildRequest(GetBudgetMethod.create()) + + /** + * Builds a sign_message request event. + */ + suspend fun signMessage(message: String): LnZapPaymentRequestEvent = buildRequest(SignMessageMethod.create(message)) + + /** + * Builds a make_hold_invoice request event. + */ + suspend fun makeHoldInvoice( + amount: Long, + paymentHash: String, + description: String? = null, + descriptionHash: String? = null, + expiry: Long? = null, + minCltvExpiryDelta: Int? = null, + ): LnZapPaymentRequestEvent = + buildRequest(MakeHoldInvoiceMethod.create(amount, paymentHash, description, descriptionHash, expiry, minCltvExpiryDelta)) + + /** + * Builds a cancel_hold_invoice request event. + */ + suspend fun cancelHoldInvoice(paymentHash: String): LnZapPaymentRequestEvent = + buildRequest(CancelHoldInvoiceMethod.create(paymentHash)) + + /** + * Builds a settle_hold_invoice request event. + */ + suspend fun settleHoldInvoice(preimage: String): LnZapPaymentRequestEvent = + buildRequest(SettleHoldInvoiceMethod.create(preimage)) + + /** + * Builds a request event from any [Request] object. + * This is the low-level method used by all convenience methods above. + */ + suspend fun buildRequest(request: Request): LnZapPaymentRequestEvent = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletPubKeyHex, + signer = signer, + useNip44 = useNip44, + ) + + // --- Response handling --- + + /** + * Decrypts and parses a response event from the wallet. + */ + suspend fun parseResponse(event: LnZapPaymentResponseEvent): Response = event.decrypt(signer) + + /** + * Decrypts and parses a notification event from the wallet. + */ + suspend fun parseNotification(event: NwcNotificationEvent): Notification = event.decryptNotification(signer) + + // --- Filter helpers --- + + /** + * Creates a filter to subscribe for responses to a specific request. + * Use this to subscribe on [relayUrl] after sending a request event. + */ + fun responseFilter(requestEventId: HexKey): Filter = + Filter( + kinds = listOf(LnZapPaymentResponseEvent.KIND), + authors = listOf(walletPubKeyHex), + tags = mapOf("e" to listOf(requestEventId)), + ) + + /** + * Creates a filter to subscribe for all responses from the wallet + * directed to this client. + */ + fun allResponsesFilter(since: Long? = null): Filter = + Filter( + kinds = listOf(LnZapPaymentResponseEvent.KIND), + authors = listOf(walletPubKeyHex), + tags = mapOf("p" to listOf(signer.pubKey)), + since = since, + ) + + /** + * Creates a filter to subscribe for wallet notifications. + */ + fun notificationsFilter(since: Long? = null): Filter = + Filter( + kinds = listOf(NwcNotificationEvent.KIND, NwcNotificationEvent.LEGACY_KIND), + authors = listOf(walletPubKeyHex), + tags = mapOf("p" to listOf(signer.pubKey)), + since = since, + ) + + /** + * Creates a filter to fetch the wallet's info event (kind 13194). + */ + fun infoFilter(): Filter = + Filter( + kinds = listOf(NwcInfoEvent.KIND), + authors = listOf(walletPubKeyHex), + limit = 1, + ) +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt new file mode 100644 index 0000000000..6b032f3ae2 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner + +/** + * High-level NIP-47 Wallet Connect server (wallet service). + * + * Simplifies building a wallet service that receives NWC requests from clients, + * processes them, and sends back responses and notifications. + * + * Usage: + * ```kotlin + * val server = Nip47Server(walletSigner, supportedMethods, relayUrl) + * + * // Publish capabilities + * val infoEvent = server.buildInfoEvent() + * // Send infoEvent to relay + * + * // Subscribe using server.requestsFilter() on your relay + * + * // When a request arrives: + * val request = server.parseRequest(requestEvent) + * when (request) { + * is GetBalanceMethod -> { + * val response = server.respondGetBalance(requestEvent, balance = 2100000L) + * // Send response to relay + * } + * is PayInvoiceMethod -> { + * // Process payment, then: + * val response = server.respondPayInvoice(requestEvent, preimage = "abc123") + * // Or on error: + * val error = server.respondError(requestEvent, NwcErrorCode.PAYMENT_FAILED, "Route not found") + * // Send response to relay + * } + * } + * ``` + */ +class Nip47Server( + val signer: NostrSigner, + val capabilities: List = emptyList(), + val useNip44: Boolean = false, + val encryptionSchemes: List? = null, + val notificationTypes: List? = null, +) { + // --- Info event --- + + /** + * Builds a kind 13194 info event advertising wallet capabilities. + * Sign and publish this event to your relay. + */ + fun buildInfoEvent() = + NwcInfoEvent.build( + capabilities = capabilities, + encryptionSchemes = encryptionSchemes, + notificationTypes = notificationTypes, + ) + + // --- Request parsing --- + + /** + * Decrypts and parses an incoming client request. + */ + suspend fun parseRequest(event: LnZapPaymentRequestEvent): Request = event.decryptRequest(signer) + + // --- Response builders --- + + /** + * Builds a response event from any [Response] object. + */ + suspend fun buildResponse( + response: Response, + requestEvent: LnZapPaymentRequestEvent, + ): LnZapPaymentResponseEvent = + LnZapPaymentResponseEvent.createResponse( + response = response, + requestEvent = requestEvent, + signer = signer, + useNip44 = useNip44, + ) + + /** + * Builds an error response for any method. + */ + suspend fun respondError( + requestEvent: LnZapPaymentRequestEvent, + code: NwcErrorCode, + message: String, + resultType: String? = null, + ): LnZapPaymentResponseEvent { + val method = resultType ?: requestEvent.decryptRequest(signer).method ?: NwcMethod.PAY_INVOICE + return buildResponse(NwcErrorResponse(method, NwcError(code, message)), requestEvent) + } + + /** + * Builds a pay_invoice success response. + */ + suspend fun respondPayInvoice( + requestEvent: LnZapPaymentRequestEvent, + preimage: String? = null, + feesPaid: Long? = null, + ): LnZapPaymentResponseEvent = + buildResponse( + PayInvoiceSuccessResponse(PayInvoiceSuccessResponse.PayInvoiceResultParams(preimage, feesPaid)), + requestEvent, + ) + + /** + * Builds a pay_keysend success response. + */ + suspend fun respondPayKeysend( + requestEvent: LnZapPaymentRequestEvent, + preimage: String? = null, + feesPaid: Long? = null, + ): LnZapPaymentResponseEvent = + buildResponse( + PayKeysendSuccessResponse(PayKeysendSuccessResponse.PayKeysendResult(preimage, feesPaid)), + requestEvent, + ) + + /** + * Builds a get_balance success response. + */ + suspend fun respondGetBalance( + requestEvent: LnZapPaymentRequestEvent, + balance: Long, + ): LnZapPaymentResponseEvent = + buildResponse( + GetBalanceSuccessResponse(GetBalanceSuccessResponse.GetBalanceResult(balance)), + requestEvent, + ) + + /** + * Builds a get_info success response. + */ + suspend fun respondGetInfo( + requestEvent: LnZapPaymentRequestEvent, + alias: String? = null, + color: String? = null, + pubkey: String? = null, + network: String? = null, + blockHeight: Long? = null, + blockHash: String? = null, + methods: List? = null, + notifications: List? = null, + lud16: String? = null, + ): LnZapPaymentResponseEvent = + buildResponse( + GetInfoSuccessResponse( + GetInfoSuccessResponse.GetInfoResult( + alias, color, pubkey, network, blockHeight, blockHash, methods, notifications, null, lud16, + ), + ), + requestEvent, + ) + + /** + * Builds a make_invoice success response. + */ + suspend fun respondMakeInvoice( + requestEvent: LnZapPaymentRequestEvent, + transaction: NwcTransaction, + ): LnZapPaymentResponseEvent = + buildResponse(MakeInvoiceSuccessResponse(transaction), requestEvent) + + /** + * Builds a lookup_invoice success response. + */ + suspend fun respondLookupInvoice( + requestEvent: LnZapPaymentRequestEvent, + transaction: NwcTransaction, + ): LnZapPaymentResponseEvent = + buildResponse(LookupInvoiceSuccessResponse(transaction), requestEvent) + + /** + * Builds a list_transactions success response. + */ + suspend fun respondListTransactions( + requestEvent: LnZapPaymentRequestEvent, + transactions: List, + totalCount: Long? = null, + ): LnZapPaymentResponseEvent = + buildResponse( + ListTransactionsSuccessResponse( + ListTransactionsSuccessResponse.ListTransactionsResult(transactions, totalCount), + ), + requestEvent, + ) + + /** + * Builds a get_budget success response. + */ + suspend fun respondGetBudget( + requestEvent: LnZapPaymentRequestEvent, + usedBudget: Long? = null, + totalBudget: Long? = null, + renewsAt: Long? = null, + renewalPeriod: String? = null, + ): LnZapPaymentResponseEvent = + buildResponse( + GetBudgetSuccessResponse( + GetBudgetSuccessResponse.GetBudgetResult(usedBudget, totalBudget, renewsAt, renewalPeriod), + ), + requestEvent, + ) + + /** + * Builds a sign_message success response. + */ + suspend fun respondSignMessage( + requestEvent: LnZapPaymentRequestEvent, + message: String, + signature: String, + ): LnZapPaymentResponseEvent = + buildResponse( + SignMessageSuccessResponse(SignMessageSuccessResponse.SignMessageResult(message, signature)), + requestEvent, + ) + + // --- Notification builders --- + + /** + * Builds a payment_received notification event. + */ + suspend fun notifyPaymentReceived( + clientPubkey: HexKey, + transaction: NwcTransaction, + ): NwcNotificationEvent = + NwcNotificationEvent.createNotification( + notification = PaymentReceivedNotification(transaction), + clientPubkey = clientPubkey, + signer = signer, + ) + + /** + * Builds a payment_sent notification event. + */ + suspend fun notifyPaymentSent( + clientPubkey: HexKey, + transaction: NwcTransaction, + ): NwcNotificationEvent = + NwcNotificationEvent.createNotification( + notification = PaymentSentNotification(transaction), + clientPubkey = clientPubkey, + signer = signer, + ) + + /** + * Builds a notification event from any [Notification] object. + */ + suspend fun buildNotification( + notification: Notification, + clientPubkey: HexKey, + ): NwcNotificationEvent = + NwcNotificationEvent.createNotification( + notification = notification, + clientPubkey = clientPubkey, + signer = signer, + ) + + // --- Filter helpers --- + + /** + * Creates a filter to subscribe for incoming client requests. + * Use this to subscribe on your relay. + */ + fun requestsFilter(since: Long? = null): Filter = + Filter( + kinds = listOf(LnZapPaymentRequestEvent.KIND), + tags = mapOf("p" to listOf(signer.pubKey)), + since = since, + ) +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md index 95907efa97..0d372e1a49 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/README.md @@ -4,10 +4,86 @@ Quartz implementation of [NIP-47](https://github.com/nostr-protocol/nips/blob/ma Wallet Connect (NWC). This module provides everything needed to build both **wallet client apps** (like Amethyst) and **wallet service backends** (like Alby Hub). +## Quick Start — Wallet Client + +Use `Nip47Client` for a high-level API that handles URI parsing, signer creation, +event building, filter construction, and response decryption: + +```kotlin +// 1. Create client from NWC URI +val client = Nip47Client.fromUri("nostr+walletconnect://pubkey?relay=...&secret=...") + +// 2. Build request events — one method per NWC command +val payEvent = client.payInvoice("lnbc50n1...") +val balanceEvent = client.getBalance() +val infoEvent = client.getInfo() +val invoiceEvent = client.makeInvoice(amount = 50000L, description = "Coffee") +val txEvent = client.listTransactions(limit = 20) + +// 3. Send event to client.relayUrl via your relay connection +// 4. Subscribe using client.responseFilter(payEvent.id) for the response + +// 5. When response arrives, parse it +val response = client.parseResponse(responseEvent) +when (response) { + is PayInvoiceSuccessResponse -> println("Paid! Preimage: ${response.result?.preimage}") + is GetBalanceSuccessResponse -> println("Balance: ${response.result?.balance} msats") + is NwcErrorResponse -> println("Error: ${response.error?.message}") +} + +// Filter helpers for relay subscriptions +val filter = client.responseFilter(payEvent.id) // Filter for a specific response +val allFilter = client.allResponsesFilter() // Filter for all responses +val notifFilter = client.notificationsFilter() // Filter for notifications +val walletInfo = client.infoFilter() // Filter for wallet info event +``` + +## Quick Start — Wallet Service + +Use `Nip47Server` to build a wallet service that receives requests and sends responses: + +```kotlin +// 1. Create server +val server = Nip47Server( + signer = walletSigner, + capabilities = listOf(NwcMethod.PAY_INVOICE, NwcMethod.GET_BALANCE, NwcMethod.GET_INFO), +) + +// 2. Publish capabilities (kind 13194) +val infoTemplate = server.buildInfoEvent() +// Sign and send: walletSigner.sign(infoTemplate) + +// 3. Subscribe using server.requestsFilter() on your relay + +// 4. When a request arrives, parse and respond +val request = server.parseRequest(requestEvent) +when (request) { + is GetBalanceMethod -> { + val response = server.respondGetBalance(requestEvent, balance = 2100000L) + // Send response to relay + } + is PayInvoiceMethod -> { + // Process payment, then: + val response = server.respondPayInvoice(requestEvent, preimage = "abc123") + // Or on error: + val error = server.respondError(requestEvent, NwcErrorCode.PAYMENT_FAILED, "Route not found") + } + is MakeInvoiceMethod -> { + val tx = NwcTransaction(type = NwcTransactionType.INCOMING, invoice = "lnbc...") + val response = server.respondMakeInvoice(requestEvent, tx) + } +} + +// 5. Send notifications +val notifEvent = server.notifyPaymentReceived(clientPubkey, transaction) +``` + ## Architecture ``` nip47WalletConnect/ +├── Nip47Client.kt # High-level client API (URI → requests → responses) +├── Nip47Server.kt # High-level server API (requests → responses → notifications) ├── Nip47WalletConnect.kt # URI parsing (nostr+walletconnect://) ├── Request.kt # All 13 NWC request methods + params ├── Response.kt # All response types (success + error) @@ -38,49 +114,42 @@ nip47WalletConnect/ ## Supported Methods -| Method | Request Class | Success Response Class | -|----------------------|---------------------------|-----------------------------------| -| `pay_invoice` | `PayInvoiceMethod` | `PayInvoiceSuccessResponse` | -| `pay_keysend` | `PayKeysendMethod` | `PayKeysendSuccessResponse` | -| `make_invoice` | `MakeInvoiceMethod` | `MakeInvoiceSuccessResponse` | -| `lookup_invoice` | `LookupInvoiceMethod` | `LookupInvoiceSuccessResponse` | -| `list_transactions` | `ListTransactionsMethod` | `ListTransactionsSuccessResponse` | -| `get_balance` | `GetBalanceMethod` | `GetBalanceSuccessResponse` | -| `get_info` | `GetInfoMethod` | `GetInfoSuccessResponse` | -| `get_budget` | `GetBudgetMethod` | `GetBudgetSuccessResponse` | -| `sign_message` | `SignMessageMethod` | `SignMessageSuccessResponse` | -| `create_connection` | `CreateConnectionMethod` | `CreateConnectionSuccessResponse` | -| `make_hold_invoice` | `MakeHoldInvoiceMethod` | `MakeHoldInvoiceSuccessResponse` | -| `cancel_hold_invoice`| `CancelHoldInvoiceMethod` | `CancelHoldInvoiceSuccessResponse`| -| `settle_hold_invoice`| `SettleHoldInvoiceMethod` | `SettleHoldInvoiceSuccessResponse`| +| Method | `Nip47Client` method | Request Class | Success Response Class | +|----------------------|-----------------------------|---------------------------|-----------------------------------| +| `pay_invoice` | `payInvoice()` | `PayInvoiceMethod` | `PayInvoiceSuccessResponse` | +| `pay_keysend` | `payKeysend()` | `PayKeysendMethod` | `PayKeysendSuccessResponse` | +| `make_invoice` | `makeInvoice()` | `MakeInvoiceMethod` | `MakeInvoiceSuccessResponse` | +| `lookup_invoice` | `lookupInvoiceByHash/ByInvoice()` | `LookupInvoiceMethod`| `LookupInvoiceSuccessResponse` | +| `list_transactions` | `listTransactions()` | `ListTransactionsMethod` | `ListTransactionsSuccessResponse` | +| `get_balance` | `getBalance()` | `GetBalanceMethod` | `GetBalanceSuccessResponse` | +| `get_info` | `getInfo()` | `GetInfoMethod` | `GetInfoSuccessResponse` | +| `get_budget` | `getBudget()` | `GetBudgetMethod` | `GetBudgetSuccessResponse` | +| `sign_message` | `signMessage()` | `SignMessageMethod` | `SignMessageSuccessResponse` | +| `create_connection` | `buildRequest()` | `CreateConnectionMethod` | `CreateConnectionSuccessResponse` | +| `make_hold_invoice` | `makeHoldInvoice()` | `MakeHoldInvoiceMethod` | `MakeHoldInvoiceSuccessResponse` | +| `cancel_hold_invoice`| `cancelHoldInvoice()` | `CancelHoldInvoiceMethod` | `CancelHoldInvoiceSuccessResponse`| +| `settle_hold_invoice`| `settleHoldInvoice()` | `SettleHoldInvoiceMethod` | `SettleHoldInvoiceSuccessResponse`| Any method can also return `NwcErrorResponse` or (for `pay_invoice`) `PayInvoiceErrorResponse`. -## Implementing a Wallet Client +## Low-Level API -A wallet client connects to a user's lightning wallet to send payments, check -balances, create invoices, and list transactions. +The high-level `Nip47Client` and `Nip47Server` classes wrap the lower-level event +builders. You can use these directly if you need more control. -### 1. Parse the NWC Connection URI +### Wallet Client (Low-Level) -Users provide an NWC connection string from their wallet provider: +#### 1. Parse the NWC Connection URI ```kotlin val uri = "nostr+walletconnect://b889ff5b...?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c..." val nwcConfig = Nip47WalletConnect.parse(uri) - -// nwcConfig.pubKeyHex — wallet service pubkey -// nwcConfig.relayUri — relay to communicate through (NormalizedRelayUrl) -// nwcConfig.secret — hex secret for the client signer -// nwcConfig.lud16 — optional lightning address ``` Supported URI schemes: `nostr+walletconnect://`, `nostrwalletconnect://`, `amethyst+walletconnect://` -### 2. Create the Client Signer - -The `secret` from the URI becomes the client's signing key: +#### 2. Create the Client Signer ```kotlin val clientSigner = NostrSignerInternal( @@ -88,12 +157,9 @@ val clientSigner = NostrSignerInternal( ) ``` -### 3. Build and Send Requests - -Use `LnZapPaymentRequestEvent.createRequest()` to create encrypted request events: +#### 3. Build and Send Requests ```kotlin -// Get balance val balanceRequest = GetBalanceMethod.create() val event = LnZapPaymentRequestEvent.createRequest( request = balanceRequest, @@ -101,36 +167,6 @@ val event = LnZapPaymentRequestEvent.createRequest( signer = clientSigner, ) // Send `event` to `nwcConfig.relayUri` - -// Pay an invoice -val payRequest = PayInvoiceMethod.create("lnbc50n1...") -val payEvent = LnZapPaymentRequestEvent.createRequest( - request = payRequest, - walletServicePubkey = nwcConfig.pubKeyHex, - signer = clientSigner, -) - -// Create an invoice (amount in millisats) -val invoiceRequest = MakeInvoiceMethod.create( - amount = 50000L, // 50 sats in millisats - description = "Coffee", -) -val invoiceEvent = LnZapPaymentRequestEvent.createRequest( - request = invoiceRequest, - walletServicePubkey = nwcConfig.pubKeyHex, - signer = clientSigner, -) - -// List transactions with pagination -val listRequest = ListTransactionsMethod.create( - limit = 20, - offset = 0, -) -val listEvent = LnZapPaymentRequestEvent.createRequest( - request = listRequest, - walletServicePubkey = nwcConfig.pubKeyHex, - signer = clientSigner, -) ``` To use NIP-44 encryption instead of NIP-04: @@ -144,202 +180,69 @@ val event = LnZapPaymentRequestEvent.createRequest( ) ``` -### 4. Receive and Parse Responses +#### 4. Receive and Parse Responses Subscribe to kind `23195` events on the NWC relay, filtered by the wallet -service pubkey and the request event ID. When a response arrives: +service pubkey and the request event ID: ```kotlin -// responseEvent is a LnZapPaymentResponseEvent (kind 23195) val response: Response = responseEvent.decrypt(clientSigner) when (response) { is GetBalanceSuccessResponse -> { - val balanceMillisats = response.result?.balance ?: 0L - val balanceSats = balanceMillisats / 1000L + val balanceSats = (response.result?.balance ?: 0L) / 1000L } is PayInvoiceSuccessResponse -> { val preimage = response.result?.preimage - val feesPaid = response.result?.fees_paid - } - is MakeInvoiceSuccessResponse -> { - val bolt11 = response.result?.invoice - val paymentHash = response.result?.payment_hash - } - is ListTransactionsSuccessResponse -> { - val transactions: List = response.result?.transactions ?: emptyList() - transactions.forEach { tx -> - // tx.type — "incoming" or "outgoing" - // tx.amount — in millisats - // tx.description, tx.created_at, tx.state, etc. - } - } - is GetInfoSuccessResponse -> { - val alias = response.result?.alias - val methods = response.result?.methods // supported methods - val lud16 = response.result?.lud16 - } - is PayInvoiceErrorResponse -> { - val errorCode = response.error?.code // NwcErrorCode enum - val errorMessage = response.error?.message } is NwcErrorResponse -> { - val errorCode = response.error?.code val errorMessage = response.error?.message } } ``` -### 5. Listen for Notifications (Optional) - -Subscribe to kind `23196`/`23197` events from the wallet: +#### 5. Listen for Notifications ```kotlin -// notificationEvent is an NwcNotificationEvent val notification: Notification = notificationEvent.decryptNotification(clientSigner) when (notification) { is PaymentReceivedNotification -> { val tx: NwcTransaction? = notification.notification - // tx?.amount, tx?.description, tx?.payment_hash, etc. } is PaymentSentNotification -> { val tx: NwcTransaction? = notification.notification } - is HoldInvoiceAcceptedNotification -> { - val data = notification.notification - // data?.payment_hash, data?.amount, data?.settle_deadline - } } ``` -### 6. Transaction State Helpers +### Wallet Service (Low-Level) -Transaction states from different wallet implementations may use different -casing. Use the case-insensitive helpers: +#### 1. Publish Capabilities ```kotlin -val tx: NwcTransaction = ... - -NwcTransactionState.isSettled(tx.state) // true for "SETTLED" or "settled" -NwcTransactionState.isPending(tx.state) // true for "PENDING" or "pending" -NwcTransactionState.isFailed(tx.state) // true for "FAILED" or "failed" -NwcTransactionState.isAccepted(tx.state) // true for "ACCEPTED" or "accepted" -``` - -### 7. URI Persistence - -To save/restore the NWC connection: - -```kotlin -// Save -val nip47URI: Nip47WalletConnect.Nip47URI = nwcConfig.denormalize()!! -val json = Nip47WalletConnect.Nip47URI.serializer(nip47URI) - -// Restore -val restored = Nip47WalletConnect.Nip47URI.parser(json) -val normalized = restored.normalize()!! -``` - -## Implementing a Wallet Service - -A wallet service receives NWC requests from clients, processes them (e.g., -pays invoices via a Lightning node), and sends back responses. - -### 1. Publish Capabilities - -Advertise which methods your wallet supports by publishing a kind `13194` -event: - -```kotlin -val capabilities = listOf( - NwcMethod.PAY_INVOICE, - NwcMethod.GET_BALANCE, - NwcMethod.GET_INFO, - NwcMethod.MAKE_INVOICE, - NwcMethod.LOOKUP_INVOICE, - NwcMethod.LIST_TRANSACTIONS, -) - val infoTemplate = NwcInfoEvent.build( - capabilities = capabilities, + capabilities = listOf(NwcMethod.PAY_INVOICE, NwcMethod.GET_BALANCE), encryptionSchemes = listOf("nip04", "nip44_v2"), - notificationTypes = listOf( - NwcNotificationType.PAYMENT_RECEIVED, - NwcNotificationType.PAYMENT_SENT, - ), + notificationTypes = listOf(NwcNotificationType.PAYMENT_RECEIVED), ) // Sign with wallet signer: walletSigner.sign(infoTemplate) ``` -### 2. Receive and Parse Requests - -Subscribe to kind `23194` events on your relay, filtered by your wallet -service pubkey in the `p` tag. When a request arrives: +#### 2. Parse Requests and Build Responses ```kotlin -// requestEvent is a LnZapPaymentRequestEvent (kind 23194) val request: Request = requestEvent.decryptRequest(walletSigner) -when (request) { - is PayInvoiceMethod -> { - val bolt11 = request.params?.invoice - val amount = request.params?.amount // optional override in millisats - // Process payment via your Lightning node... - } - is GetBalanceMethod -> { - // Query your Lightning node for balance... - } - is MakeInvoiceMethod -> { - val amount = request.params?.amount // in millisats - val description = request.params?.description - // Create invoice via your Lightning node... - } - is ListTransactionsMethod -> { - val limit = request.params?.limit - val offset = request.params?.offset - val type = request.params?.type // "incoming" or "outgoing" - // Query transaction history... - } - is GetInfoMethod -> { - // Return node info... - } - is GetBudgetMethod -> { - // Return budget info... - } - // ... handle other methods -} -``` - -### 3. Build and Send Responses - -Use `LnZapPaymentResponseEvent.createResponse()` to create encrypted -response events: - -```kotlin -// Success response for get_balance +// Build response val balanceResponse = GetBalanceSuccessResponse( - GetBalanceSuccessResponse.GetBalanceResult(balance = 2100000L) // in millisats + GetBalanceSuccessResponse.GetBalanceResult(balance = 2100000L) ) val responseEvent = LnZapPaymentResponseEvent.createResponse( response = balanceResponse, requestEvent = requestEvent, signer = walletSigner, ) -// Send responseEvent to the relay - -// Success response for pay_invoice -val payResponse = PayInvoiceSuccessResponse( - PayInvoiceSuccessResponse.PayInvoiceResultParams( - preimage = "0123456789abcdef", - fees_paid = 100L, - ) -) -val payResponseEvent = LnZapPaymentResponseEvent.createResponse( - response = payResponse, - requestEvent = requestEvent, - signer = walletSigner, -) // Error response val errorResponse = NwcErrorResponse( @@ -353,60 +256,46 @@ val errorEvent = LnZapPaymentResponseEvent.createResponse( ) ``` -To use NIP-44 encryption for responses: +#### 3. Send Notifications ```kotlin -val responseEvent = LnZapPaymentResponseEvent.createResponse( - response = balanceResponse, - requestEvent = requestEvent, - signer = walletSigner, - useNip44 = true, -) -``` - -### 4. Send Notifications - -Push notifications to clients for payment events: - -```kotlin -// Payment received notification -val notification = PaymentReceivedNotification( - notification = NwcTransaction( - type = NwcTransactionType.INCOMING, - state = NwcTransactionState.SETTLED, - invoice = "lnbc...", - amount = 50000L, // in millisats - payment_hash = "abc123", - settled_at = TimeUtils.now(), - created_at = TimeUtils.now(), - ), -) val notifEvent = NwcNotificationEvent.createNotification( - notification = notification, + notification = PaymentReceivedNotification( + notification = NwcTransaction( + type = NwcTransactionType.INCOMING, + state = NwcTransactionState.SETTLED, + invoice = "lnbc...", + amount = 50000L, + payment_hash = "abc123", + settled_at = TimeUtils.now(), + created_at = TimeUtils.now(), + ), + ), clientPubkey = clientPubkeyHex, signer = walletSigner, ) -// Send notifEvent to the relay ``` -### 5. Build Transaction Objects +## Transaction State Helpers -Transactions are used across multiple response types: +Transaction states from different wallet implementations may use different +casing. Use the case-insensitive helpers: ```kotlin -val transaction = NwcTransaction( - type = NwcTransactionType.INCOMING, // or OUTGOING - state = NwcTransactionState.SETTLED, // PENDING, SETTLED, FAILED, ACCEPTED - invoice = "lnbc50n1...", - description = "Coffee payment", - payment_hash = "abc123def456", - preimage = "fedcba654321", - amount = 50000L, // in millisats - fees_paid = 100L, // in millisats - created_at = 1693876497L, // unix timestamp - settled_at = 1693876500L, - expires_at = 1694876497L, -) +NwcTransactionState.isSettled(tx.state) // true for "SETTLED" or "settled" +NwcTransactionState.isPending(tx.state) // true for "PENDING" or "pending" +NwcTransactionState.isFailed(tx.state) // true for "FAILED" or "failed" +NwcTransactionState.isAccepted(tx.state) // true for "ACCEPTED" or "accepted" +``` + +## URI Persistence + +```kotlin +// Save +val json = Nip47WalletConnect.Nip47URI.serializer(nwcConfig.denormalize()!!) + +// Restore +val restored = Nip47WalletConnect.Nip47URI.parser(json).normalize()!! ``` ## Error Codes @@ -431,13 +320,8 @@ val transaction = NwcTransaction( NWC supports two encryption schemes: -- **NIP-04** (default): Set `useNip44 = false` in event builders -- **NIP-44 v2**: Set `useNip44 = true` in event builders - -When building requests with NIP-44, the event includes an `encryption` tag: -``` -["encryption", "nip44_v2"] -``` +- **NIP-04** (default): `Nip47Client(useNip44 = false)` or `useNip44 = false` in event builders +- **NIP-44 v2**: `Nip47Client(useNip44 = true)` or `useNip44 = true` in event builders Clients can check a wallet's supported encryption via the info event: ```kotlin @@ -454,8 +338,6 @@ For apps handling many concurrent NWC events, use the built-in LRU caches: val requestCache = NostrWalletConnectRequestCache(signer) val responseCache = NostrWalletConnectResponseCache(signer) -// These cache up to 50 decrypted results and handle -// async retry logic for permission dialogs and timeouts. val request: Request? = requestCache.decryptRequest(requestEvent) val response: Response? = responseCache.decryptResponse(responseEvent) ``` From 493fe05ac41df28a6a8c72f0d8d5de6f32a6489d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 14 Mar 2026 10:18:08 -0400 Subject: [PATCH 080/172] Fixes missing parameter --- .../com/vitorpamplona/quartz/nip47WalletConnect/Request.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt index cdce4c7ea3..26e5711a3e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt @@ -129,7 +129,9 @@ class ListTransactionsMethod( offset: Int? = null, unpaid: Boolean? = null, type: String? = null, - ): ListTransactionsMethod = ListTransactionsMethod(ListTransactionsParams(from, until, limit, offset, unpaid, type)) + unpaid_outgoing: Boolean? = null, + unpaid_incoming: Boolean? = null, + ): ListTransactionsMethod = ListTransactionsMethod(ListTransactionsParams(from, until, limit, offset, unpaid, unpaid_outgoing, unpaid_incoming, type)) } } From 120981f8c4ecad4c17ec33da39741d5b054d8a89 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 14 Mar 2026 10:18:16 -0400 Subject: [PATCH 081/172] spotless apply --- .../ui/navigation/drawer/DrawerContent.kt | 2 +- .../screen/loggedIn/wallet/WalletViewModel.kt | 31 +++++--- .../quartz/nip47WalletConnect/Nip47Client.kt | 15 ++-- .../quartz/nip47WalletConnect/Nip47Server.kt | 17 +++-- .../nip47WalletConnect/AlbyInteropTest.kt | 16 ++++- .../quartz/nip47WalletConnect/RequestTest.kt | 16 ++++- .../quartz/nip47WalletConnect/ResponseTest.kt | 32 +++++++-- .../jackson/ResponseDeserializer.kt | 70 +++++++++++++++---- 8 files changed, 152 insertions(+), 47 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index a3682c723d..139ad10c35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -46,10 +46,10 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.outlined.AccountBalanceWallet import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.Drafts import androidx.compose.material.icons.outlined.GroupAdd -import androidx.compose.material.icons.outlined.AccountBalanceWallet import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt index 817c296e29..69a5840258 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -112,9 +112,11 @@ class WalletViewModel : ViewModel() { // NWC balance is in millisats, convert to sats _balanceSats.value = (response.result?.balance ?: 0L) / 1000L } + is NwcErrorResponse -> { _error.value = response.error?.message ?: "Balance request failed" } + else -> {} } _isLoading.value = false @@ -135,6 +137,7 @@ class WalletViewModel : ViewModel() { is GetInfoSuccessResponse -> { _walletAlias.value = response.result?.alias } + else -> {} } } @@ -163,9 +166,11 @@ class WalletViewModel : ViewModel() { is ListTransactionsSuccessResponse -> { _transactions.value = response.result?.transactions ?: emptyList() } + is NwcErrorResponse -> { _error.value = response.error?.message ?: "Failed to load transactions" } + else -> {} } _isLoading.value = false @@ -189,16 +194,21 @@ class WalletViewModel : ViewModel() { // Refresh balance after payment fetchBalance() } + is PayInvoiceErrorResponse -> { - _sendState.value = SendState.Error( - response.error?.message ?: "Payment failed", - ) + _sendState.value = + SendState.Error( + response.error?.message ?: "Payment failed", + ) } + is NwcErrorResponse -> { - _sendState.value = SendState.Error( - response.error?.message ?: "Payment failed", - ) + _sendState.value = + SendState.Error( + response.error?.message ?: "Payment failed", + ) } + else -> { _sendState.value = SendState.Error("Unexpected response") } @@ -234,11 +244,14 @@ class WalletViewModel : ViewModel() { _receiveState.value = ReceiveState.Error("No invoice returned") } } + is NwcErrorResponse -> { - _receiveState.value = ReceiveState.Error( - response.error?.message ?: "Invoice creation failed", - ) + _receiveState.value = + ReceiveState.Error( + response.error?.message ?: "Invoice creation failed", + ) } + else -> { _receiveState.value = ReceiveState.Error("Unexpected response") } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt index 5686fc1d53..05cb6e21f0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Client.kt @@ -137,14 +137,12 @@ class Nip47Client( /** * Builds a lookup_invoice request event by payment hash. */ - suspend fun lookupInvoiceByHash(paymentHash: String): LnZapPaymentRequestEvent = - buildRequest(LookupInvoiceMethod.createByHash(paymentHash)) + suspend fun lookupInvoiceByHash(paymentHash: String): LnZapPaymentRequestEvent = buildRequest(LookupInvoiceMethod.createByHash(paymentHash)) /** * Builds a lookup_invoice request event by BOLT11 invoice. */ - suspend fun lookupInvoiceByInvoice(invoice: String): LnZapPaymentRequestEvent = - buildRequest(LookupInvoiceMethod.createByInvoice(invoice)) + suspend fun lookupInvoiceByInvoice(invoice: String): LnZapPaymentRequestEvent = buildRequest(LookupInvoiceMethod.createByInvoice(invoice)) /** * Builds a list_transactions request event. @@ -178,20 +176,17 @@ class Nip47Client( descriptionHash: String? = null, expiry: Long? = null, minCltvExpiryDelta: Int? = null, - ): LnZapPaymentRequestEvent = - buildRequest(MakeHoldInvoiceMethod.create(amount, paymentHash, description, descriptionHash, expiry, minCltvExpiryDelta)) + ): LnZapPaymentRequestEvent = buildRequest(MakeHoldInvoiceMethod.create(amount, paymentHash, description, descriptionHash, expiry, minCltvExpiryDelta)) /** * Builds a cancel_hold_invoice request event. */ - suspend fun cancelHoldInvoice(paymentHash: String): LnZapPaymentRequestEvent = - buildRequest(CancelHoldInvoiceMethod.create(paymentHash)) + suspend fun cancelHoldInvoice(paymentHash: String): LnZapPaymentRequestEvent = buildRequest(CancelHoldInvoiceMethod.create(paymentHash)) /** * Builds a settle_hold_invoice request event. */ - suspend fun settleHoldInvoice(preimage: String): LnZapPaymentRequestEvent = - buildRequest(SettleHoldInvoiceMethod.create(preimage)) + suspend fun settleHoldInvoice(preimage: String): LnZapPaymentRequestEvent = buildRequest(SettleHoldInvoiceMethod.create(preimage)) /** * Builds a request event from any [Request] object. diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt index 6b032f3ae2..947bbecf82 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Nip47Server.kt @@ -169,7 +169,16 @@ class Nip47Server( buildResponse( GetInfoSuccessResponse( GetInfoSuccessResponse.GetInfoResult( - alias, color, pubkey, network, blockHeight, blockHash, methods, notifications, null, lud16, + alias, + color, + pubkey, + network, + blockHeight, + blockHash, + methods, + notifications, + null, + lud16, ), ), requestEvent, @@ -181,8 +190,7 @@ class Nip47Server( suspend fun respondMakeInvoice( requestEvent: LnZapPaymentRequestEvent, transaction: NwcTransaction, - ): LnZapPaymentResponseEvent = - buildResponse(MakeInvoiceSuccessResponse(transaction), requestEvent) + ): LnZapPaymentResponseEvent = buildResponse(MakeInvoiceSuccessResponse(transaction), requestEvent) /** * Builds a lookup_invoice success response. @@ -190,8 +198,7 @@ class Nip47Server( suspend fun respondLookupInvoice( requestEvent: LnZapPaymentRequestEvent, transaction: NwcTransaction, - ): LnZapPaymentResponseEvent = - buildResponse(LookupInvoiceSuccessResponse(transaction), requestEvent) + ): LnZapPaymentResponseEvent = buildResponse(LookupInvoiceSuccessResponse(transaction), requestEvent) /** * Builds a list_transactions success response. diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt index c201ab2850..6d1d0adb79 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt @@ -334,8 +334,20 @@ class AlbyInteropTest { assertEquals("018465013e2337234a7e5530a21c4a8cf70d84231f4a8ff0b1e2cce3cb2bd03b", request.params?.preimage) assertNotNull(request.params?.tlv_records) assertEquals(1, request.params?.tlv_records?.size) - assertEquals(5482373484L, request.params?.tlv_records?.first()?.type) - assertEquals("fajsn341414fq", request.params?.tlv_records?.first()?.value) + assertEquals( + 5482373484L, + request.params + ?.tlv_records + ?.first() + ?.type, + ) + assertEquals( + "fajsn341414fq", + request.params + ?.tlv_records + ?.first() + ?.value, + ) } @Test diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt index 3299b09275..21816b6473 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt @@ -93,8 +93,20 @@ class RequestTest { assertEquals("preimage123", request.params?.preimage) assertNotNull(request.params?.tlv_records) assertEquals(1, request.params?.tlv_records?.size) - assertEquals(7629169L, request.params?.tlv_records?.first()?.type) - assertEquals("hex_value", request.params?.tlv_records?.first()?.value) + assertEquals( + 7629169L, + request.params + ?.tlv_records + ?.first() + ?.type, + ) + assertEquals( + "hex_value", + request.params + ?.tlv_records + ?.first() + ?.value, + ) } @Test diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt index 1484833605..c26e05bf01 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt @@ -128,10 +128,34 @@ class ResponseTest { assertIs(response) assertNotNull(response.result?.transactions) assertEquals(2, response.result?.transactions?.size) - assertEquals("incoming", response.result?.transactions?.get(0)?.type) - assertEquals(100L, response.result?.transactions?.get(0)?.amount) - assertEquals("outgoing", response.result?.transactions?.get(1)?.type) - assertEquals(200L, response.result?.transactions?.get(1)?.amount) + assertEquals( + "incoming", + response.result + ?.transactions + ?.get(0) + ?.type, + ) + assertEquals( + 100L, + response.result + ?.transactions + ?.get(0) + ?.amount, + ) + assertEquals( + "outgoing", + response.result + ?.transactions + ?.get(1) + ?.type, + ) + assertEquals( + 200L, + response.result + ?.transactions + ?.get(1) + ?.amount, + ) } @Test diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt index c9b210ad95..ccb98c8b74 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseDeserializer.kt @@ -56,7 +56,10 @@ class ResponseDeserializer : StdDeserializer(Response::class.java) { if (hasError) { return when (resultType) { - NwcMethod.PAY_INVOICE -> jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java) + NwcMethod.PAY_INVOICE -> { + jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java) + } + else -> { val error = jp.codec.treeToValue(jsonObject.get("error"), NwcError::class.java) NwcErrorResponse(resultType ?: "", error) @@ -66,19 +69,58 @@ class ResponseDeserializer : StdDeserializer(Response::class.java) { if (hasResult || resultType != null) { return when (resultType) { - NwcMethod.PAY_INVOICE -> jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java) - NwcMethod.PAY_KEYSEND -> jp.codec.treeToValue(jsonObject, PayKeysendSuccessResponse::class.java) - NwcMethod.MAKE_INVOICE -> jp.codec.treeToValue(jsonObject, MakeInvoiceSuccessResponse::class.java) - NwcMethod.LOOKUP_INVOICE -> jp.codec.treeToValue(jsonObject, LookupInvoiceSuccessResponse::class.java) - NwcMethod.LIST_TRANSACTIONS -> jp.codec.treeToValue(jsonObject, ListTransactionsSuccessResponse::class.java) - NwcMethod.GET_BALANCE -> jp.codec.treeToValue(jsonObject, GetBalanceSuccessResponse::class.java) - NwcMethod.GET_INFO -> jp.codec.treeToValue(jsonObject, GetInfoSuccessResponse::class.java) - NwcMethod.GET_BUDGET -> jp.codec.treeToValue(jsonObject, GetBudgetSuccessResponse::class.java) - NwcMethod.SIGN_MESSAGE -> jp.codec.treeToValue(jsonObject, SignMessageSuccessResponse::class.java) - NwcMethod.CREATE_CONNECTION -> jp.codec.treeToValue(jsonObject, CreateConnectionSuccessResponse::class.java) - NwcMethod.MAKE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, MakeHoldInvoiceSuccessResponse::class.java) - NwcMethod.CANCEL_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, CancelHoldInvoiceSuccessResponse::class.java) - NwcMethod.SETTLE_HOLD_INVOICE -> jp.codec.treeToValue(jsonObject, SettleHoldInvoiceSuccessResponse::class.java) + NwcMethod.PAY_INVOICE -> { + jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java) + } + + NwcMethod.PAY_KEYSEND -> { + jp.codec.treeToValue(jsonObject, PayKeysendSuccessResponse::class.java) + } + + NwcMethod.MAKE_INVOICE -> { + jp.codec.treeToValue(jsonObject, MakeInvoiceSuccessResponse::class.java) + } + + NwcMethod.LOOKUP_INVOICE -> { + jp.codec.treeToValue(jsonObject, LookupInvoiceSuccessResponse::class.java) + } + + NwcMethod.LIST_TRANSACTIONS -> { + jp.codec.treeToValue(jsonObject, ListTransactionsSuccessResponse::class.java) + } + + NwcMethod.GET_BALANCE -> { + jp.codec.treeToValue(jsonObject, GetBalanceSuccessResponse::class.java) + } + + NwcMethod.GET_INFO -> { + jp.codec.treeToValue(jsonObject, GetInfoSuccessResponse::class.java) + } + + NwcMethod.GET_BUDGET -> { + jp.codec.treeToValue(jsonObject, GetBudgetSuccessResponse::class.java) + } + + NwcMethod.SIGN_MESSAGE -> { + jp.codec.treeToValue(jsonObject, SignMessageSuccessResponse::class.java) + } + + NwcMethod.CREATE_CONNECTION -> { + jp.codec.treeToValue(jsonObject, CreateConnectionSuccessResponse::class.java) + } + + NwcMethod.MAKE_HOLD_INVOICE -> { + jp.codec.treeToValue(jsonObject, MakeHoldInvoiceSuccessResponse::class.java) + } + + NwcMethod.CANCEL_HOLD_INVOICE -> { + jp.codec.treeToValue(jsonObject, CancelHoldInvoiceSuccessResponse::class.java) + } + + NwcMethod.SETTLE_HOLD_INVOICE -> { + jp.codec.treeToValue(jsonObject, SettleHoldInvoiceSuccessResponse::class.java) + } + else -> { // tries to guess for backward compatibility if (jsonObject.get("result")?.get("preimage") != null) { From 2fd623a97d3c0f3bc2751f63e62c3c31933b4da4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 14:30:38 +0000 Subject: [PATCH 082/172] fix: wallet screen showing "No wallet connected" on first open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WalletViewModel.init() was called inside LaunchedEffect (async), but hasWalletSetup() was checked synchronously during the first composition frame—before init had run. Moved init to run synchronously during composition and made hasWalletSetup a reactive StateFlow so the UI updates when wallet state changes. https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt | 7 +++---- .../amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt | 8 +++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt index ef0d948000..8ef7878156 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletScreen.kt @@ -73,10 +73,9 @@ fun WalletScreen( nav: INav, ) { val walletViewModel: WalletViewModel = viewModel() + walletViewModel.init(accountViewModel.account) - LaunchedEffect(accountViewModel) { - walletViewModel.init(accountViewModel.account) - } + val hasWallet by walletViewModel.hasWalletSetup.collectAsState() Scaffold( topBar = { @@ -93,7 +92,7 @@ fun WalletScreen( ) }, ) { padding -> - if (!walletViewModel.hasWalletSetup()) { + if (!hasWallet) { NoWalletSetup( modifier = Modifier.padding(padding), nav = nav, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt index 69a5840258..ff670665df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -73,6 +73,9 @@ sealed class ReceiveState { class WalletViewModel : ViewModel() { private var account: Account? = null + private val _hasWalletSetup = MutableStateFlow(false) + val hasWalletSetup = _hasWalletSetup.asStateFlow() + private val _balanceSats = MutableStateFlow(null) val balanceSats = _balanceSats.asStateFlow() @@ -96,9 +99,12 @@ class WalletViewModel : ViewModel() { fun init(account: Account) { this.account = account + _hasWalletSetup.value = account.nip47SignerState?.hasWalletConnectSetup() == true } - fun hasWalletSetup(): Boolean = account?.nip47SignerState?.hasWalletConnectSetup() == true + fun refreshWalletSetup() { + _hasWalletSetup.value = account?.nip47SignerState?.hasWalletConnectSetup() == true + } fun fetchBalance() { val acc = account ?: return From 84035f887a6cc90e01b9dab390a6dd175f65af29 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 15:18:53 +0000 Subject: [PATCH 083/172] feat: parse NIP-47 transaction metadata for sender/recipient display Add NwcTransactionMetadata parser that extracts comment, payer_data, recipient_data, and nostr zap data from the untyped metadata field. The transaction list UI now shows the Nostr user profile picture and name for zap senders/recipients, falls back to payer name/email or lightning address, and displays comment when it differs from description. https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../wallet/WalletTransactionsScreen.kt | 158 ++++++++++++++---- .../nip47WalletConnect/NwcTransaction.kt | 4 +- .../NwcTransactionMetadata.kt | 106 ++++++++++++ .../nip47WalletConnect/AlbyInteropTest.kt | 72 ++++++++ 4 files changed, 311 insertions(+), 29 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransactionMetadata.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt index a7df6653b8..fb3a97ddd1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt @@ -59,7 +59,10 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.UserPicture +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransaction import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransactionType @@ -143,7 +146,7 @@ fun WalletTransactionsScreen( modifier = Modifier.padding(padding), ) { items(transactions) { tx -> - TransactionItem(tx) + TransactionItem(tx, accountViewModel, nav) HorizontalDivider() } } @@ -152,7 +155,11 @@ fun WalletTransactionsScreen( } @Composable -private fun TransactionItem(tx: NwcTransaction) { +private fun TransactionItem( + tx: NwcTransaction, + accountViewModel: AccountViewModel, + nav: INav, +) { val isIncoming = tx.type == NwcTransactionType.INCOMING val amountSats = (tx.amount ?: 0L) / 1000L val formattedAmount = @@ -169,6 +176,36 @@ private fun TransactionItem(tx: NwcTransaction) { } ?: "" } + val parsed = remember(tx.metadata) { tx.parsedMetadata() } + + // For incoming: show who sent it (nostr pubkey or payer name/email) + // For outgoing: show who received it (nostr recipient or recipient identifier) + val counterpartyPubkeyHex = + remember(parsed) { + if (isIncoming) parsed?.senderPubkeyHex() else parsed?.recipientPubkeyHex() + } + + val counterpartyDisplayName = + remember(parsed) { + if (isIncoming) { + parsed?.senderDisplayName() + } else { + parsed?.recipientIdentifier() + } + } + + // Show comment only if it differs from description + val commentText = + remember(parsed, tx.description) { + parsed?.comment?.let { comment -> + if (tx.description == null || !comment.equals(tx.description, ignoreCase = true)) { + comment + } else { + null + } + } + } + Row( modifier = Modifier @@ -176,34 +213,74 @@ private fun TransactionItem(tx: NwcTransaction) { .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - imageVector = - if (isIncoming) Icons.Filled.ArrowDownward else Icons.Filled.ArrowUpward, - contentDescription = - if (isIncoming) { - stringRes(R.string.wallet_incoming) - } else { - stringRes(R.string.wallet_outgoing) - }, - modifier = Modifier.size(24.dp), - tint = - if (isIncoming) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - ) - - Spacer(modifier = Modifier.width(12.dp)) + if (counterpartyPubkeyHex != null) { + UserPicture( + userHex = counterpartyPubkeyHex, + size = 40.dp, + accountViewModel = accountViewModel, + nav = nav, + ) + Spacer(modifier = Modifier.width(12.dp)) + } else { + Icon( + imageVector = + if (isIncoming) Icons.Filled.ArrowDownward else Icons.Filled.ArrowUpward, + contentDescription = + if (isIncoming) { + stringRes(R.string.wallet_incoming) + } else { + stringRes(R.string.wallet_outgoing) + }, + modifier = Modifier.size(24.dp), + tint = + if (isIncoming) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + Spacer(modifier = Modifier.width(12.dp)) + } Column(modifier = Modifier.weight(1f)) { - Text( - text = tx.description ?: if (isIncoming) stringRes(R.string.wallet_incoming) else stringRes(R.string.wallet_outgoing), - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Medium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + if (counterpartyPubkeyHex != null) { + TransactionUserName(counterpartyPubkeyHex, counterpartyDisplayName, accountViewModel) + } else if (counterpartyDisplayName != null) { + Text( + text = counterpartyDisplayName, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else { + Text( + text = tx.description ?: if (isIncoming) stringRes(R.string.wallet_incoming) else stringRes(R.string.wallet_outgoing), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + if (commentText != null) { + Text( + text = commentText, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } else if (counterpartyPubkeyHex != null || counterpartyDisplayName != null) { + val descOrType = tx.description ?: if (isIncoming) stringRes(R.string.wallet_incoming) else stringRes(R.string.wallet_outgoing) + Text( + text = descOrType, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Text( text = dateText, style = MaterialTheme.typography.bodySmall, @@ -224,3 +301,28 @@ private fun TransactionItem(tx: NwcTransaction) { ) } } + +@Composable +private fun TransactionUserName( + pubkeyHex: String, + fallbackName: String?, + accountViewModel: AccountViewModel, +) { + LoadUser(baseUserHex = pubkeyHex, accountViewModel = accountViewModel) { user -> + if (user != null) { + UsernameDisplay( + baseUser = user, + fontWeight = FontWeight.Medium, + accountViewModel = accountViewModel, + ) + } else { + Text( + text = fallbackName ?: pubkeyHex.take(8) + "...", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt index 067f12cd9c..adff64b56c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt @@ -63,7 +63,9 @@ class NwcTransaction( var settled_at: Long? = null, var settle_deadline: Long? = null, var metadata: Any? = null, -) +) { + fun parsedMetadata(): NwcTransactionMetadata? = NwcTransactionMetadata.parse(metadata) +} class TlvRecord( var type: Long? = null, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransactionMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransactionMetadata.kt new file mode 100644 index 0000000000..a18be518ae --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransactionMetadata.kt @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip19Bech32.decodePublicKeyAsHexOrNull + +class NwcTransactionMetadata( + val comment: String?, + val payerData: PayerData?, + val recipientData: RecipientData?, + val nostr: NostrZapData?, +) { + class PayerData( + val name: String?, + val email: String?, + val pubkey: String?, + ) + + class RecipientData( + val identifier: String?, + ) + + class NostrZapData( + val pubkeyHex: String?, + val recipientPubkeyHex: String?, + ) + + fun senderPubkeyHex(): String? = nostr?.pubkeyHex ?: payerData?.pubkey?.let { decodePublicKeyAsHexOrNull(it) } + + fun senderDisplayName(): String? = payerData?.name ?: payerData?.email + + fun recipientIdentifier(): String? = recipientData?.identifier + + fun recipientPubkeyHex(): String? = nostr?.recipientPubkeyHex + + companion object { + fun parse(metadata: Any?): NwcTransactionMetadata? { + val map = metadata as? Map<*, *> ?: return null + + val comment = map["comment"] as? String + + val payerData = (map["payer_data"] as? Map<*, *>)?.let { pd -> + PayerData( + name = pd["name"] as? String, + email = pd["email"] as? String, + pubkey = pd["pubkey"] as? String, + ) + } + + val recipientData = (map["recipient_data"] as? Map<*, *>)?.let { rd -> + RecipientData( + identifier = rd["identifier"] as? String, + ) + } + + val nostr = (map["nostr"] as? Map<*, *>)?.let { n -> + val rawPubkey = n["pubkey"] as? String + val pubkeyHex = rawPubkey?.let { decodePublicKeyAsHexOrNull(it) } + + val tags = n["tags"] as? List<*> + val recipientHex = tags?.firstNotNullOfOrNull { tag -> + val tagList = tag as? List<*> + if (tagList != null && tagList.size >= 2 && tagList[0] == "p") { + tagList[1] as? String + } else { + null + } + } + + NostrZapData( + pubkeyHex = pubkeyHex, + recipientPubkeyHex = recipientHex, + ) + } + + if (comment == null && payerData == null && recipientData == null && nostr == null) { + return null + } + + return NwcTransactionMetadata( + comment = comment, + payerData = payerData, + recipientData = recipientData, + nostr = nostr, + ) + } + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt index 6d1d0adb79..f6221cdc62 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt @@ -500,6 +500,78 @@ class AlbyInteropTest { assertNotNull(response.result?.metadata) } + @Test + fun testMetadataParserComment() { + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"settled_at":2000,"metadata":{"comment":"Great post!"}}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val parsed = response.result?.parsedMetadata() + assertNotNull(parsed) + assertEquals("Great post!", parsed.comment) + assertNull(parsed.payerData) + assertNull(parsed.nostr) + } + + @Test + fun testMetadataParserPayerData() { + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"settled_at":2000,"metadata":{"payer_data":{"name":"Alice","email":"alice@example.com","pubkey":"abc123"}}}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val parsed = response.result?.parsedMetadata() + assertNotNull(parsed) + assertEquals("Alice", parsed.payerData?.name) + assertEquals("alice@example.com", parsed.payerData?.email) + assertEquals("abc123", parsed.payerData?.pubkey) + assertEquals("Alice", parsed.senderDisplayName()) + } + + @Test + fun testMetadataParserNostrZap() { + val senderHex = "7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e" + val recipientHex = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c" + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash","amount":21000,"created_at":1000,"settled_at":2000,"metadata":{"nostr":{"pubkey":"$senderHex","tags":[["p","$recipientHex"],["amount","21000"]]}}}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val parsed = response.result?.parsedMetadata() + assertNotNull(parsed) + assertEquals(senderHex, parsed.senderPubkeyHex()) + assertEquals(recipientHex, parsed.recipientPubkeyHex()) + } + + @Test + fun testMetadataParserRecipientData() { + val json = + """{"result_type":"lookup_invoice","result":{"type":"outgoing","state":"settled","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"settled_at":2000,"metadata":{"recipient_data":{"identifier":"alice@getalby.com"}}}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val parsed = response.result?.parsedMetadata() + assertNotNull(parsed) + assertEquals("alice@getalby.com", parsed.recipientIdentifier()) + } + + @Test + fun testMetadataParserNullForSimpleMetadata() { + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"settled_at":2000,"metadata":{"a":123}}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val parsed = response.result?.parsedMetadata() + assertNull(parsed) + } + + @Test + fun testMetadataParserNullMetadata() { + val json = + """{"result_type":"lookup_invoice","result":{"type":"incoming","state":"settled","invoice":"lnbc...","payment_hash":"hash","amount":5000,"created_at":1000,"settled_at":2000}}""" + val response = OptimizedJsonMapper.fromJsonTo(json) + assertIs(response) + val parsed = response.result?.parsedMetadata() + assertNull(parsed) + } + @Test fun testJsSdkGetInfoWithAllMethods() { // JS SDK advertises all 13 single methods + notifications From 03e1c32757035f65901f2561f5b97521fbf8b351 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 14 Mar 2026 11:20:33 -0400 Subject: [PATCH 084/172] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../quartz/nip47WalletConnect/NwcNotificationEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt index 9e9af6b2cb..d5884bbb19 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcNotificationEvent.kt @@ -48,7 +48,7 @@ class NwcNotificationEvent( suspend fun decryptNotification(signer: NostrSigner): Notification { if (!canDecrypt(signer)) throw SignerExceptions.UnauthorizedDecryptionException() - val jsonText = signer.nip44Decrypt(content, talkingWith(signer.pubKey)) + val jsonText = signer.decrypt(content, talkingWith(signer.pubKey)) return OptimizedJsonMapper.fromJsonTo(jsonText) } From 78c66b00ebf0600d8805a325709fd3b5fd16fc9d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Mar 2026 15:29:19 +0000 Subject: [PATCH 085/172] fix: align arrow icons to 40dp to match user picture size https://claude.ai/code/session_01JFogiwyR4CPVP8cRznqahJ --- .../ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt index fb3a97ddd1..0f18188099 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletTransactionsScreen.kt @@ -231,7 +231,7 @@ private fun TransactionItem( } else { stringRes(R.string.wallet_outgoing) }, - modifier = Modifier.size(24.dp), + modifier = Modifier.size(40.dp), tint = if (isIncoming) { MaterialTheme.colorScheme.primary From d4ae3411c7ffd30d745b593a734bbcbb95086ee2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 14 Mar 2026 11:36:58 -0400 Subject: [PATCH 086/172] spotless --- .../NwcTransactionMetadata.kt | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransactionMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransactionMetadata.kt index a18be518ae..a275ac4695 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransactionMetadata.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransactionMetadata.kt @@ -57,39 +57,43 @@ class NwcTransactionMetadata( val comment = map["comment"] as? String - val payerData = (map["payer_data"] as? Map<*, *>)?.let { pd -> - PayerData( - name = pd["name"] as? String, - email = pd["email"] as? String, - pubkey = pd["pubkey"] as? String, - ) - } - - val recipientData = (map["recipient_data"] as? Map<*, *>)?.let { rd -> - RecipientData( - identifier = rd["identifier"] as? String, - ) - } - - val nostr = (map["nostr"] as? Map<*, *>)?.let { n -> - val rawPubkey = n["pubkey"] as? String - val pubkeyHex = rawPubkey?.let { decodePublicKeyAsHexOrNull(it) } - - val tags = n["tags"] as? List<*> - val recipientHex = tags?.firstNotNullOfOrNull { tag -> - val tagList = tag as? List<*> - if (tagList != null && tagList.size >= 2 && tagList[0] == "p") { - tagList[1] as? String - } else { - null - } + val payerData = + (map["payer_data"] as? Map<*, *>)?.let { pd -> + PayerData( + name = pd["name"] as? String, + email = pd["email"] as? String, + pubkey = pd["pubkey"] as? String, + ) } - NostrZapData( - pubkeyHex = pubkeyHex, - recipientPubkeyHex = recipientHex, - ) - } + val recipientData = + (map["recipient_data"] as? Map<*, *>)?.let { rd -> + RecipientData( + identifier = rd["identifier"] as? String, + ) + } + + val nostr = + (map["nostr"] as? Map<*, *>)?.let { n -> + val rawPubkey = n["pubkey"] as? String + val pubkeyHex = rawPubkey?.let { decodePublicKeyAsHexOrNull(it) } + + val tags = n["tags"] as? List<*> + val recipientHex = + tags?.firstNotNullOfOrNull { tag -> + val tagList = tag as? List<*> + if (tagList != null && tagList.size >= 2 && tagList[0] == "p") { + tagList[1] as? String + } else { + null + } + } + + NostrZapData( + pubkeyHex = pubkeyHex, + recipientPubkeyHex = recipientHex, + ) + } if (comment == null && payerData == null && recipientData == null && nostr == null) { return null From 42574f302cfcdb55aed1ac93df55799bdd5e06f4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 14 Mar 2026 11:39:02 -0400 Subject: [PATCH 087/172] updates change log --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 193605cf98..51195f4cba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Redesigns Media Player - Turn video controller creation into a flow to fix playback lifecycle issues - Adds support for uploading audio -Adds support for NIP events (kind 30817) +Adds support for NIP-47 Wallets Adds support for NIP-52 Calendar appointments @@ -23,6 +23,8 @@ Adds support for NIP-39 External Identities with kind 10011 Adds support for NIP-C0 Code Snippets +Adds support for NIPs on Nostr (event kind 30817) + Adds support for NIP-A3 Payment targets (PayTo: 10133) by @npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5 Adds support for BUD-10 "Blossom:" URIs in images, audios, videos, and documents. From 0dce9542fb7610322dca625867432f8cf1e880b1 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 14 Mar 2026 15:41:38 +0000 Subject: [PATCH 088/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-hi-rIN/strings.xml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index e733a94834..be1bd8761e 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -125,6 +125,7 @@ अष्टक अपक्रम अपक्रम + सफल संयोजनों का प्रतिशत पुनःप्रसारक के साथ संयोजन अपक्रमों की संख्या इस सत्र में मुख्य सूचनावली निजी संदेश सूचनावली @@ -150,6 +151,7 @@ सर्वनाम लै॰जाल पता लै॰जाल पता (पुराना) + संचारयन्त्र में अभिलेखन करें चित्रालय में अभिलेखन करें चित्र का अभिलेखन किया गया चित्रालय क्रमक में चलचित्र अवरोहण आरम्भ हुआ … @@ -158,6 +160,7 @@ चलचित्र को संचारयन्त्र के चलचित्रालय में सुरक्षित रखा गया चलचित्र को सुरक्षित रखने में असफल चित्र आरोहण + अभिलेख आरोहण एक चित्र लें चलचित्र का अभिलेखन करें ऐक संदेश का अभिलेखन करें @@ -429,6 +432,7 @@ ज्साप गोपनीयता नियन्त्रण करता है कि आपका परिचय कैसे दिखाया जाता है ज्साप भेजने पर। संयोजन धनकोष + पुनःप्रसारक सूचनावली देखें प्रतिज्ञा मात्रा साट्स में मतदान प्रकाशित करें अनिवार्य प्रपत्रस्थान : @@ -642,6 +646,7 @@ जुडें आज विषयवस्तु चेतावनी + सावधान : %1$s इस पत्र में संवेदनशील विषयवस्तु समावेशित है जो कुछ लोगों के लिए आपत्तिजनक अथवा व्याकुल करनेवाला लग सकता है संवेदनशील विषयवस्तु सर्वदा छिपाएँ संवेदनशील विषयवस्तु सर्वदा दिखाएँ @@ -670,6 +675,7 @@ अष्टकों में मात्रा जो इस पुनःप्रसारक से प्राप्त हुआ था छलनियाँ तथा घटनाएँ समेत %1$s से पुनःप्रसारक जानकारी प्राप्त करने के प्रयास में अपक्रम हुआ अधिपति + द्वारा उपयुक्त सेवा कुंजी %1$s चलाया जा रहा है %1$s (%2$s) चलाया जा रहा है @@ -828,6 +834,7 @@ स्थान प्राप्त किया जा रहा है स्थान प्राप्त करने की अनुमति नहीं आपके विषयवस्तु दिखाने से पूर्व संवेदनशील विषयवस्तु चेतावनी जोडता है। यह आदर्श है किसी कार्यालय अनुचित विषयवस्तु के लिए अथवा जो कुछ लोगों के लिए आपत्तिजनक अथवा व्याकुल करनेवाला लग सकता है + कारण (विकल्पात्मक) नयी सुविधा इस कार्यशैली सक्षम करने के लिए अमेथिस्ट के द्वारा निप॰-१७ संदेश (उपहारकोषयुक्त, आच्छादित सीधा तथा झुण्ड संदेश) भेजना पडेगा। यह निप॰-१७ नया है तथा अनेक ग्राहक इसे कार्यान्वित किया नहीं अब तक। सुनिश्चित करें कि प्राप्तकर्ता एक अनुकूल ग्राहक का प्रयोग कर रहे हैं। सक्रिय करें @@ -866,6 +873,7 @@ चित्रों का अवरोहण कब करें चिति की अनुकृति करें टाँकाफलक में अनुकृति करें + टाँकाफलक में एन॰परिचय की अनुकृति करें टाँकाफलक में एनपुब॰ की अनुकृति करें बाँटें अथवा अभिलेखन करें टाँकाफलक में जालपता की अनुकृति करें @@ -920,6 +928,8 @@ सुनिश्चित करें हस्ताक्षर क्रमक ने इस व्यापार को अनुमति दिया कोई धनकोष प्राप्त नहीं लैटनिंग चालान चुकाने के लिए (अपक्रम : %1$s)। कृपया एक लैटनिंग धनकोष की स्थापना करें ज्सापों का प्रयोग करने के लिए कोई धनकोष प्राप्त नहीं लैटनिंग चालान चुकाने के लिए। कृपया एक लैटनिंग धनकोष की स्थापना करें ज्सापों का प्रयोग करने के लिए + ब्लोस्सम॰ योजक खोला नहीं जा सकता + ब्लोस्सम॰ क्रमक प्राप्त नहीं। कृपया एक स्थानीय ब्लोस्सम॰ क्रमक की स्थापना करें इस अभिलेख को देखने के लिए छिपाए गये शब्द नया शब्द अथवा वाक्य छिपाएँ परिचय चित्र @@ -1043,6 +1053,7 @@ छोटे चतुरंग सुरक्षार्थ छलनियाँ + अनुचरित आयात करें नया पत्र प्रकाशन नये छोटे : चित्र अथवा चलचित्र नया सामुदायिक टीका @@ -1088,7 +1099,15 @@ ज्साप विभाजन निरस्त करें विषयवस्तु चेतावनी जोडें विषयवस्तु चेतावनी हटाएँ + समापन दिनांक जोडें + समापन दिनांक हटाएँ + समापन दिनांक + पत्र इस दिनांक के पश्चात ग्राहकों द्वारा छिपाया जाएगा (निप॰४०) + समापन दिनांक तथा समय चुनें + %1$s में समाप्त + समापन समय क्यूआर॰ क्रमचित्र के रूप में एनपुब॰ को दिखाएँ + क्यूआर॰ क्रमचित्र के रूप में एन॰परिचय को दिखाएँ अमान्य पता अमेथिस्ट को एक वैश्विक वस्तु विभेदक प्राप्त हुआ खोलने के लिए परन्तु वह विभेदक अमान्य था : %1$s सीधा संदेश आगतपेटिका पुनःप्रसारक @@ -1156,6 +1175,9 @@ बाधित पुनःप्रसारक बाधित पुनःप्रसारक अमेथिस्ट इन पुनःप्रसारकों से कभी नहीं जुडेगा + पुनःप्रसारक स्थापना विकल्प निर्यात + लेख के रूप में निर्यात + ज्सिप॰ (जेसोन॰) के रूप में निर्यात क्रमलेखकों को ज्साप करें! आपका दान हमारा सहायक है परिवर्तन लाने में। प्रत्येक साट गणनीय है! दान करें अभी @@ -1261,6 +1283,19 @@ विषयसूचक खोज : #%1$s अनुवाद ना करें यहाँ प्रस्तुत भाषाओं का अनुवाद नहीं होगा। भाषा चयन करें हटाने के लिए जिससे उसका अनुवाद पुनः होने लगेगा। + अनुवाद इस में + भाषा चयन करें जिसमें विषयवस्तु का अनुवाद करना है। + भाषा प्रस्तुति आद्यताएँ + प्रत्येक अनुवाद भाषा युग्म के लिए चयन करें किस भाषा को पहले दिखाना है। + %1$s से %2$s + भाषा ढूँढें + भाषा जोडें + भाषा युग्म जोडें + स्रोत भाषा + लक्ष्य भाषा + पहले %1$s दिखाएँ + अभी कोई भाषा प्रस्तुति आद्यताएँ नहीं। ये स्वचालित रूप से बनाए जाएँगे जब अनुवाद होगा अथवा आप इनहें स्वयम हाथ से जोड सकते हैं। + आद्यता मिटाएँ विराम चलाएँ विकल्प सूची खोलें @@ -1270,6 +1305,7 @@ क्रमदोष सूचनापत्र प्राप्त क्या आप निकटकालिक क्रमदोष सूचनापत्र एक सीधे सन्देश में अमेथिस्ट को भेजना चाहते हैं। कोई व्यक्तिगत जानकारी बाँटी नहीं जाएगी भेजें + यह सन्देश %1$s में अदृश्य हो जाएगा यह सन्देश %1$d दिनों में अदृश्य हो जाएगा हस्ताक्षरकर्ता चुनें पहले से ही सूची में @@ -1471,4 +1507,8 @@ ध्वनि सन्देश ध्वनि उत्तर विकि॰ + एक बढिया सूचनावली के साथ आरम्भ करें उन लोगों का अनुचरण करके जिनको आपके द्वारा विश्वास प्राप्त कोई व्यक्ति करते हैं। + अनुचरित सूची आयात + प्रयोक्ता चुनें अनुचरण करने के लिए + प्रयोक्ता परिचय जिससे आयात करना है From c290a1c7793c9c398f751b60f953afbd41259f36 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 12:58:18 +0100 Subject: [PATCH 089/172] - SelectFromGallery and SelectFromFiles now accept an enabled param separate from isUploading - All ViewModels track isUploadingFile alongside isUploadingImage, set via hasNonMedia() - All screen call sites pass source-specific flags so only the initiating button spins - canPost() methods block posting during file uploads too - ChatNewMessageViewModel delegates to ChatFileUploadState (fixes pre-existing dead state) - cancel() methods reset both flags in all ViewModels --- .../amethyst/ui/actions/EditPostView.kt | 4 +++- .../amethyst/ui/actions/EditPostViewModel.kt | 11 +++++++++-- .../amethyst/ui/actions/uploads/SelectFromFiles.kt | 6 ++++-- .../ui/actions/uploads/SelectFromGallery.kt | 9 ++++++--- .../ui/note/nip22Comments/CommentPostViewModel.kt | 10 +++++++++- .../note/nip22Comments/GenericCommentPostScreen.kt | 4 +++- .../privateDM/send/ChatNewMessageViewModel.kt | 4 +++- .../chats/privateDM/send/NewGroupDMScreen.kt | 4 +++- .../privateDM/send/upload/ChatFileUploader.kt | 14 ++++++++++++-- .../send/ChannelNewMessageViewModel.kt | 12 +++++++++++- .../loggedIn/chats/utils/ChatFileUploadState.kt | 2 ++ .../discover/nip99Classifieds/NewProductScreen.kt | 4 +++- .../nip99Classifieds/NewProductViewModel.kt | 10 +++++++++- .../ui/screen/loggedIn/home/ShortNotePostScreen.kt | 4 +++- .../screen/loggedIn/home/ShortNotePostViewModel.kt | 12 ++++++++++-- .../publicMessages/NewPublicMessageScreen.kt | 4 +++- .../publicMessages/NewPublicMessageViewModel.kt | 12 +++++++++++- 17 files changed, 104 insertions(+), 22 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index d27935f49a..14b9bb5cc4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -372,6 +372,7 @@ private fun BottomRowActions(postViewModel: EditPostViewModel) { ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -379,7 +380,8 @@ private fun BottomRowActions(postViewModel: EditPostViewModel) { } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 1c9d57d603..12a7482ba1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -80,6 +80,7 @@ open class EditPostViewModel : ViewModel() { var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -180,7 +181,11 @@ open class EditPostViewModel : ViewModel() { val myAccount = account val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -244,6 +249,7 @@ open class EditPostViewModel : ViewModel() { } isUploadingImage = false + isUploadingFile = false } } @@ -256,6 +262,7 @@ open class EditPostViewModel : ViewModel() { multiOrchestrator = null urlPreview = null isUploadingImage = false + isUploadingFile = false wantsInvoice = false @@ -296,7 +303,7 @@ open class EditPostViewModel : ViewModel() { } } - fun canPost() = message.text.isNotBlank() && !isUploadingImage && !wantsInvoice && multiOrchestrator == null + fun canPost() = message.text.isNotBlank() && !isUploadingImage && !isUploadingFile && !wantsInvoice && multiOrchestrator == null fun selectImage(uris: ImmutableList) { multiOrchestrator = MultiOrchestrator(uris) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt index 354853720c..7ed03deb21 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromFiles.kt @@ -48,6 +48,7 @@ import java.util.concurrent.atomic.AtomicBoolean @Composable fun SelectFromFiles( isUploading: Boolean, + enabled: Boolean = true, tint: Color, modifier: Modifier, onFilesChosen: (ImmutableList) -> Unit, @@ -64,19 +65,20 @@ fun SelectFromFiles( ) } - FileSelectButton(isUploading, tint, modifier) { showFileSelect = true } + FileSelectButton(isUploading, enabled, tint, modifier) { showFileSelect = true } } @Composable private fun FileSelectButton( isUploading: Boolean, + enabled: Boolean, tint: Color, modifier: Modifier, onClick: () -> Unit, ) { IconButton( modifier = modifier, - enabled = !isUploading, + enabled = enabled && !isUploading, onClick = { onClick() }, ) { if (!isUploading) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt index 0e38451c57..bd7eb8fe2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt @@ -69,6 +69,7 @@ class SelectedMedia( @Composable fun SelectFromGallery( isUploading: Boolean, + enabled: Boolean = true, tint: Color, modifier: Modifier, onImageChosen: (ImmutableList) -> Unit, @@ -85,12 +86,13 @@ fun SelectFromGallery( ) } - GallerySelectButton(isUploading, tint, modifier) { showGallerySelect = true } + GallerySelectButton(isUploading, enabled, tint, modifier) { showGallerySelect = true } } @Composable fun SelectSingleFromGallery( isUploading: Boolean, + enabled: Boolean = true, tint: Color, modifier: Modifier, onImageChosen: (SelectedMedia) -> Unit, @@ -107,19 +109,20 @@ fun SelectSingleFromGallery( ) } - GallerySelectButton(isUploading, tint, modifier) { showGallerySelect = true } + GallerySelectButton(isUploading, enabled, tint, modifier) { showGallerySelect = true } } @Composable private fun GallerySelectButton( isUploading: Boolean, + enabled: Boolean, tint: Color, modifier: Modifier, onClick: () -> Unit, ) { IconButton( modifier = modifier, - enabled = !isUploading, + enabled = enabled && !isUploading, onClick = { onClick() }, ) { if (!isUploading) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index c60b0a42e0..91c5662e5f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -150,6 +150,7 @@ open class CommentPostViewModel : val urlPreviews = PreviewState() var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -487,7 +488,11 @@ open class CommentPostViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -552,6 +557,7 @@ open class CommentPostViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -565,6 +571,7 @@ open class CommentPostViewModel : multiOrchestrator = null isUploadingImage = false + isUploadingFile = false notifying = null @@ -677,6 +684,7 @@ open class CommentPostViewModel : fun canPost(): Boolean = message.text.isNotBlank() && !isUploadingImage && + !isUploadingFile && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && multiOrchestrator == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index ed922a496f..5e8c197c7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -391,6 +391,7 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) { ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -398,7 +399,8 @@ private fun BottomRowActions(postViewModel: CommentPostViewModel) { } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 747920b86f..db6f377f92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -141,7 +141,8 @@ class ChatNewMessageViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) + val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false + val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -692,6 +693,7 @@ class ChatNewMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && uploadState?.isUploadingImage != true && + uploadState?.isUploadingFile != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && (toUsers.text.isNotBlank()) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index 3a2f1f8599..ef5f32b11b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -386,6 +386,7 @@ private fun BottomRowActions( if (postViewModel.room != null) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -393,7 +394,8 @@ private fun BottomRowActions( } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index ddc0018055..b6f8f3f914 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -43,7 +43,11 @@ class ChatFileUploader( onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return - viewState.isUploadingImage = true + if (orchestrator.hasNonMedia()) { + viewState.isUploadingFile = true + } else { + viewState.isUploadingImage = true + } val cipher = AESGCM() @@ -77,6 +81,7 @@ class ChatFileUploader( } viewState.isUploadingImage = false + viewState.isUploadingFile = false } // ------ @@ -90,7 +95,11 @@ class ChatFileUploader( onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return - viewState.isUploadingImage = true + if (orchestrator.hasNonMedia()) { + viewState.isUploadingFile = true + } else { + viewState.isUploadingImage = true + } val results = orchestrator.upload( @@ -121,5 +130,6 @@ class ChatFileUploader( } viewState.isUploadingImage = false + viewState.isUploadingFile = false } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 98b36ef0eb..0c6c59dcf8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -135,6 +135,7 @@ open class ChannelNewMessageViewModel : var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -337,7 +338,11 @@ open class ChannelNewMessageViewModel : val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -379,6 +384,7 @@ open class ChannelNewMessageViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -546,6 +552,9 @@ open class ChannelNewMessageViewModel : userSuggestions?.reset() userSuggestionsMainMessage = null + isUploadingImage = false + isUploadingFile = false + iMetaAttachments.reset() emojiSuggestions?.reset() @@ -633,6 +642,7 @@ open class ChannelNewMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && uploadState?.isUploadingImage != true && + uploadState?.isUploadingFile != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount != null) && uploadState?.multiOrchestrator == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index 9475afa739..384c8299e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -37,6 +37,7 @@ class ChatFileUploadState( val defaultServer: ServerName, ) { var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var selectedServer by mutableStateOf(defaultServer) var caption by mutableStateOf("") @@ -65,6 +66,7 @@ class ChatFileUploadState( fun reset() { multiOrchestrator = null isUploadingImage = false + isUploadingFile = false caption = "" selectedServer = defaultServer } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index aa651348ec..d4c3e1853e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -359,6 +359,7 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) { ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -366,7 +367,8 @@ private fun BottomRowActions(postViewModel: NewProductViewModel) { } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index be86ac1511..b2014fc667 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -134,6 +134,7 @@ open class NewProductViewModel : val urlPreviews = PreviewState() var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -399,7 +400,11 @@ open class NewProductViewModel : val myAccount = account ?: return@launch val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -445,6 +450,7 @@ open class NewProductViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -455,6 +461,7 @@ open class NewProductViewModel : multiOrchestrator = null isUploadingImage = false + isUploadingFile = false wantsInvoice = false wantsZapraiser = false @@ -576,6 +583,7 @@ open class NewProductViewModel : fun canPost(): Boolean = message.text.isNotBlank() && !isUploadingImage && + !isUploadingFile && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && title.text.isNotBlank() && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 8f799892e6..d6d473a9d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -494,6 +494,7 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -501,7 +502,8 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 422394b5bc..d9018502f4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -179,6 +179,7 @@ open class ShortNotePostViewModel : val urlPreviews = PreviewState() var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -829,7 +830,11 @@ open class ShortNotePostViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -886,6 +891,7 @@ open class ShortNotePostViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -898,6 +904,7 @@ open class ShortNotePostViewModel : multiOrchestrator = null isUploadingImage = false + isUploadingFile = false voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null @@ -1032,12 +1039,13 @@ open class ShortNotePostViewModel : fun canPost(): Boolean { // Voice messages can be posted without text (with either uploaded or pending recording) if (voiceMetadata != null || voiceRecording != null) { - return !isUploadingVoice && !isUploadingImage && processingPreset == null + return !isUploadingVoice && !isUploadingImage && !isUploadingFile && processingPreset == null } // Regular text/media posts require text return message.text.isNotBlank() && !isUploadingImage && + !isUploadingFile && !isUploadingVoice && !wantsInvoice && (!wantsZapRaiser || zapRaiserAmount.value != null) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index a442d467cd..0c2ff9fc1d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -321,6 +321,7 @@ private fun BottomRowActions( ) { SelectFromGallery( isUploading = postViewModel.isUploadingImage, + enabled = !postViewModel.isUploadingFile, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { @@ -328,7 +329,8 @@ private fun BottomRowActions( } SelectFromFiles( - isUploading = postViewModel.isUploadingImage, + isUploading = postViewModel.isUploadingFile, + enabled = !postViewModel.isUploadingImage, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index d3ccd54210..14495a37af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -150,6 +150,7 @@ class NewPublicMessageViewModel : val urlPreviews = PreviewState() var isUploadingImage by mutableStateOf(false) + var isUploadingFile by mutableStateOf(false) var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -439,7 +440,11 @@ class NewPublicMessageViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - isUploadingImage = true + if (myMultiOrchestrator.hasNonMedia()) { + isUploadingFile = true + } else { + isUploadingImage = true + } val results = myMultiOrchestrator.upload( @@ -495,6 +500,7 @@ class NewPublicMessageViewModel : } isUploadingImage = false + isUploadingFile = false } } @@ -523,6 +529,9 @@ class NewPublicMessageViewModel : userSuggestions?.reset() userSuggestionsMainMessage = null + isUploadingImage = false + isUploadingFile = false + iMetaAttachments.reset() emojiSuggestions?.reset() @@ -629,6 +638,7 @@ class NewPublicMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && !isUploadingImage && + !isUploadingFile && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && (toUsers.text.isNotBlank()) && From 42542159d456bc0b6f9c0bb1405a87e1cca20c59 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 13:06:12 +0100 Subject: [PATCH 090/172] bugfix: remove dead code bugfix: ChannelNewMessageViewModel canPost checks wrong state --- .../ui/actions/uploads/SelectFromGallery.kt | 3 +-- .../send/ChannelNewMessageViewModel.kt | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt index bd7eb8fe2a..fcae06fc33 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SelectFromGallery.kt @@ -92,7 +92,6 @@ fun SelectFromGallery( @Composable fun SelectSingleFromGallery( isUploading: Boolean, - enabled: Boolean = true, tint: Color, modifier: Modifier, onImageChosen: (SelectedMedia) -> Unit, @@ -109,7 +108,7 @@ fun SelectSingleFromGallery( ) } - GallerySelectButton(isUploading, enabled, tint, modifier) { showGallerySelect = true } + GallerySelectButton(isUploading, true, tint, modifier) { showGallerySelect = true } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 0c6c59dcf8..cd69ddf431 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -134,8 +134,8 @@ open class ChannelNewMessageViewModel : var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val isUploadingImage: Boolean get() = uploadState?.isUploadingImage ?: false + val isUploadingFile: Boolean get() = uploadState?.isUploadingFile ?: false var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -339,9 +339,9 @@ open class ChannelNewMessageViewModel : val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true + uploadState.isUploadingFile = true } else { - isUploadingImage = true + uploadState.isUploadingImage = true } val results = @@ -383,8 +383,8 @@ open class ChannelNewMessageViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + uploadState.isUploadingImage = false + uploadState.isUploadingFile = false } } @@ -552,8 +552,7 @@ open class ChannelNewMessageViewModel : userSuggestions?.reset() userSuggestionsMainMessage = null - isUploadingImage = false - isUploadingFile = false + uploadState?.reset() iMetaAttachments.reset() From 1fc0ba8628a764a99d41b953f6a854288edfb9d9 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 16:01:23 +0100 Subject: [PATCH 091/172] refactor: extract MediaUploadTracker with common methods bugfixes: ChatFileUploadState.canPost() was missing isUploadingFile check ChatNewMessageViewModel.cancel() was never resetting upload flags Double-reset in ChatFileUploader and ChannelNewMessageViewModel --- .../amethyst/ui/actions/EditPostViewModel.kt | 20 ++++---- .../ui/actions/uploads/MediaUploadTracker.kt | 47 +++++++++++++++++++ .../nip22Comments/CommentPostViewModel.kt | 21 ++++----- .../privateDM/send/ChatNewMessageViewModel.kt | 4 +- .../privateDM/send/upload/ChatFileUploader.kt | 18 ++----- .../send/ChannelNewMessageViewModel.kt | 12 ++--- .../chats/utils/ChatFileUploadState.kt | 11 +++-- .../nip99Classifieds/NewProductViewModel.kt | 21 ++++----- .../loggedIn/home/ShortNotePostViewModel.kt | 23 ++++----- .../NewPublicMessageViewModel.kt | 21 ++++----- 10 files changed, 103 insertions(+), 95 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index 12a7482ba1..fec34ac2a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.userSuggestions.UserSuggestionState @@ -79,8 +80,9 @@ open class EditPostViewModel : ViewModel() { var message by mutableStateOf(TextFieldValue("")) var urlPreview by mutableStateOf(null) - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -181,11 +183,7 @@ open class EditPostViewModel : ViewModel() { val myAccount = account val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -248,8 +246,7 @@ open class EditPostViewModel : ViewModel() { onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -261,8 +258,7 @@ open class EditPostViewModel : ViewModel() { multiOrchestrator = null urlPreview = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() wantsInvoice = false @@ -303,7 +299,7 @@ open class EditPostViewModel : ViewModel() { } } - fun canPost() = message.text.isNotBlank() && !isUploadingImage && !isUploadingFile && !wantsInvoice && multiOrchestrator == null + fun canPost() = message.text.isNotBlank() && !mediaUploadTracker.isUploading && !wantsInvoice && multiOrchestrator == null fun selectImage(uris: ImmutableList) { multiOrchestrator = MultiOrchestrator(uris) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt new file mode 100644 index 0000000000..6ff5e72ec8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/MediaUploadTracker.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.uploads + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +class MediaUploadTracker { + var isUploadingImage by mutableStateOf(false) + private set + var isUploadingFile by mutableStateOf(false) + private set + + val isUploading: Boolean get() = isUploadingImage || isUploadingFile + + fun startUpload(hasNonMedia: Boolean) { + if (hasNonMedia) { + isUploadingFile = true + } else { + isUploadingImage = true + } + } + + fun finishUpload() { + isUploadingImage = false + isUploadingFile = false + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 91c5662e5f..b8682a34d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState @@ -149,8 +150,9 @@ open class CommentPostViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -488,11 +490,7 @@ open class CommentPostViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -556,8 +554,7 @@ open class CommentPostViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -570,8 +567,7 @@ open class CommentPostViewModel : externalIdentity = null multiOrchestrator = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() notifying = null @@ -683,8 +679,7 @@ open class CommentPostViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - !isUploadingImage && - !isUploadingFile && + !mediaUploadTracker.isUploading && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && multiOrchestrator == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index db6f377f92..8ae6448bc1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -561,6 +561,7 @@ class ChatNewMessageViewModel : userSuggestionsMainMessage = null uploadsWaitingToBeSent = emptyList() + uploadState?.reset() iMetaAttachments.reset() @@ -692,8 +693,7 @@ class ChatNewMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - uploadState?.isUploadingImage != true && - uploadState?.isUploadingFile != true && + uploadState?.mediaUploadTracker?.isUploading != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && (toUsers.text.isNotBlank()) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt index b6f8f3f914..33a9e456e7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/upload/ChatFileUploader.kt @@ -43,11 +43,7 @@ class ChatFileUploader( onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return - if (orchestrator.hasNonMedia()) { - viewState.isUploadingFile = true - } else { - viewState.isUploadingImage = true - } + viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia()) val cipher = AESGCM() @@ -80,8 +76,7 @@ class ChatFileUploader( onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - viewState.isUploadingImage = false - viewState.isUploadingFile = false + viewState.mediaUploadTracker.finishUpload() } // ------ @@ -95,11 +90,7 @@ class ChatFileUploader( onceUploaded: suspend (List) -> Unit, ) { val orchestrator = viewState.multiOrchestrator ?: return - if (orchestrator.hasNonMedia()) { - viewState.isUploadingFile = true - } else { - viewState.isUploadingImage = true - } + viewState.mediaUploadTracker.startUpload(orchestrator.hasNonMedia()) val results = orchestrator.upload( @@ -129,7 +120,6 @@ class ChatFileUploader( onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - viewState.isUploadingImage = false - viewState.isUploadingFile = false + viewState.mediaUploadTracker.finishUpload() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index cd69ddf431..b9eecab72a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -338,11 +338,7 @@ open class ChannelNewMessageViewModel : val myMultiOrchestrator = uploadState.multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - uploadState.isUploadingFile = true - } else { - uploadState.isUploadingImage = true - } + uploadState.mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -383,8 +379,7 @@ open class ChannelNewMessageViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - uploadState.isUploadingImage = false - uploadState.isUploadingFile = false + uploadState.mediaUploadTracker.finishUpload() } } @@ -640,8 +635,7 @@ open class ChannelNewMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - uploadState?.isUploadingImage != true && - uploadState?.isUploadingFile != true && + uploadState?.mediaUploadTracker?.isUploading != true && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount != null) && uploadState?.multiOrchestrator == null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt index 384c8299e4..32105a29c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/utils/ChatFileUploadState.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.setValue import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import kotlinx.collections.immutable.ImmutableList @@ -36,8 +37,9 @@ import kotlinx.collections.immutable.ImmutableList class ChatFileUploadState( val defaultServer: ServerName, ) { - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var selectedServer by mutableStateOf(defaultServer) var caption by mutableStateOf("") @@ -65,8 +67,7 @@ class ChatFileUploadState( fun reset() { multiOrchestrator = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() caption = "" selectedServer = defaultServer } @@ -75,7 +76,7 @@ class ChatFileUploadState( multiOrchestrator?.remove(selected) } - fun canPost(): Boolean = !isUploadingImage && multiOrchestrator != null + fun canPost(): Boolean = !mediaUploadTracker.isUploading && multiOrchestrator != null fun hasPickedMedia() = multiOrchestrator != null diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index b2014fc667..c3d359d086 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState @@ -133,8 +134,9 @@ open class NewProductViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -400,11 +402,7 @@ open class NewProductViewModel : val myAccount = account ?: return@launch val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -449,8 +447,7 @@ open class NewProductViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -460,8 +457,7 @@ open class NewProductViewModel : message = TextFieldValue("") multiOrchestrator = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() wantsInvoice = false wantsZapraiser = false @@ -582,8 +578,7 @@ open class NewProductViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - !isUploadingImage && - !isUploadingFile && + !mediaUploadTracker.isUploading && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && title.text.isNotBlank() && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index d9018502f4..4b63c9df40 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -49,6 +49,7 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing @@ -178,8 +179,9 @@ open class ShortNotePostViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -830,11 +832,7 @@ open class ShortNotePostViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -890,8 +888,7 @@ open class ShortNotePostViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -903,8 +900,7 @@ open class ShortNotePostViewModel : forkedFromNote = null multiOrchestrator = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null @@ -1039,13 +1035,12 @@ open class ShortNotePostViewModel : fun canPost(): Boolean { // Voice messages can be posted without text (with either uploaded or pending recording) if (voiceMetadata != null || voiceRecording != null) { - return !isUploadingVoice && !isUploadingImage && !isUploadingFile && processingPreset == null + return !isUploadingVoice && !mediaUploadTracker.isUploading && processingPreset == null } // Regular text/media posts require text return message.text.isNotBlank() && - !isUploadingImage && - !isUploadingFile && + !mediaUploadTracker.isUploading && !isUploadingVoice && !wantsInvoice && (!wantsZapRaiser || zapRaiserAmount.value != null) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 14495a37af..fbda70c24d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -45,6 +45,7 @@ import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName +import com.vitorpamplona.amethyst.ui.actions.uploads.MediaUploadTracker import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState @@ -149,8 +150,9 @@ class NewPublicMessageViewModel : val urlPreviews = PreviewState() - var isUploadingImage by mutableStateOf(false) - var isUploadingFile by mutableStateOf(false) + val mediaUploadTracker = MediaUploadTracker() + val isUploadingImage: Boolean get() = mediaUploadTracker.isUploadingImage + val isUploadingFile: Boolean get() = mediaUploadTracker.isUploadingFile var userSuggestions: UserSuggestionState? = null var userSuggestionsMainMessage: UserSuggestionAnchor? = null @@ -440,11 +442,7 @@ class NewPublicMessageViewModel : viewModelScope.launch(Dispatchers.IO) { val myMultiOrchestrator = multiOrchestrator ?: return@launch - if (myMultiOrchestrator.hasNonMedia()) { - isUploadingFile = true - } else { - isUploadingImage = true - } + mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) val results = myMultiOrchestrator.upload( @@ -499,8 +497,7 @@ class NewPublicMessageViewModel : onError(stringRes(context, R.string.failed_to_upload_media_no_details), errorMessages.joinToString(".\n")) } - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() } } @@ -529,8 +526,7 @@ class NewPublicMessageViewModel : userSuggestions?.reset() userSuggestionsMainMessage = null - isUploadingImage = false - isUploadingFile = false + mediaUploadTracker.finishUpload() iMetaAttachments.reset() @@ -637,8 +633,7 @@ class NewPublicMessageViewModel : fun canPost(): Boolean = message.text.isNotBlank() && - !isUploadingImage && - !isUploadingFile && + !mediaUploadTracker.isUploading && !wantsInvoice && (!wantsZapraiser || zapRaiserAmount.value != null) && (toUsers.text.isNotBlank()) && From 795da8cf1b4e32b5d68fdecc8d00063f41d30aec Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 17:54:51 +0100 Subject: [PATCH 092/172] Block Add to message button while uploading --- .../java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt | 1 + .../amethyst/ui/note/creators/uploads/ImageVideoDescription.kt | 2 ++ .../amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt | 1 + .../ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt | 1 + .../loggedIn/discover/nip99Classifieds/NewProductScreen.kt | 1 + .../amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt | 1 + .../notifications/publicMessages/NewPublicMessageScreen.kt | 1 + 7 files changed, 8 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt index 14b9bb5cc4..f4978575bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostView.kt @@ -264,6 +264,7 @@ fun EditPostView( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, sensitiveContent, mediaQuality, false, server, accountViewModel.toastManager::toast, context) accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt index 9e40f6e1d7..09f9339dc6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/uploads/ImageVideoDescription.kt @@ -78,6 +78,7 @@ import kotlinx.collections.immutable.toImmutableList fun ImageVideoDescription( uris: MultiOrchestrator, defaultServer: ServerName, + isUploading: Boolean, onAdd: (String, ServerName, Boolean, Int, Boolean) -> Unit, onDelete: (SelectedMediaProcessing) -> Unit, onCancel: () -> Unit, @@ -319,6 +320,7 @@ fun ImageVideoDescription( Modifier .fillMaxWidth() .padding(vertical = 10.dp), + enabled = !isUploading, onClick = { onAdd(message, selectedServer, sensitiveContent, mediaQualitySlider, useH265Codec) }, shape = QuoteBorder, colors = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 5e8c197c7d..c4f8268251 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -300,6 +300,7 @@ private fun GenericCommentPostBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index ef5f32b11b..c8f1bf6c92 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -287,6 +287,7 @@ fun GroupDMScreenContent( ImageVideoDescription( selectedFiles, accountViewModel.account.settings.defaultFileServer, + isUploading = uploading.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.uploadAndHold( accountViewModel.toastManager::toast, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index d4c3e1853e..0400191e6d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -269,6 +269,7 @@ private fun NewProductBody( ImageVideoDescription( uris = it, defaultServer = accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index d6d473a9d3..7239f8e829 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -356,6 +356,7 @@ private fun NewPostScreenBody( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, useH265 -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context, useH265) accountViewModel.account.settings.changeDefaultFileServer(server) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 0c2ff9fc1d..d236275d1e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -246,6 +246,7 @@ fun PublicMessageScreenContent( ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, + isUploading = postViewModel.mediaUploadTracker.isUploading, onAdd = { alt, server, sensitiveContent, mediaQuality, _ -> postViewModel.upload(alt, if (sensitiveContent) "" else null, mediaQuality, server, accountViewModel.toastManager::toast, context) accountViewModel.account.settings.changeDefaultFileServer(server) From 64bf1d5090331b9ba0ebb9967f158ad34bcb24f8 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 14 Mar 2026 21:00:31 +0100 Subject: [PATCH 093/172] update cz, pt, de, sv --- .../src/main/res/values-cs-rCZ/strings.xml | 24 +++++++++++++++++ .../src/main/res/values-de-rDE/strings.xml | 26 ++++++++++++++++++- .../src/main/res/values-pt-rBR/strings.xml | 26 ++++++++++++++++++- .../src/main/res/values-sv-rSE/strings.xml | 24 +++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index e12ec2833a..3cbc5e20df 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1529,4 +1529,28 @@ %1$d%% dostupnost Nastavení Namecoin Průzkumník Bitcoin (OTS) + Peněženka + Zůstatek + Odeslat + Přijmout + Transakce + Žádná peněženka není připojena + Nastavte připojení Nostr Wallet Connect (NWC) v nastavení zapů, abyste mohli peněženku používat. + Nastavit peněženku + sats + Vložte fakturu BOLT-11 + Zaplatit + Platba úspěšná + Odesílání platby… + Částka (sats) + Popis (volitelné) + Vytvořit fakturu + Vytváření faktury… + Kopírovat fakturu + Zatím žádné transakce + Načítání… + Přijato + Odesláno + Obnovit + %1$s → %2$s diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index a1df34e69a..477dd288cf 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1510,7 +1510,7 @@ anz der Bedingungen ist erforderlich Folgeliste importieren Benutzer zum Folgen auswählen Profil, aus dem importiert werden soll - Namenssuche, npub1…, alice@example.com + Namenssuche, npub1…, alice@example.com Unterstützt npub, NIP-05, Hex und Namecoin (.bit / d/ / id/) Folgeliste nachschlagen Tipp @@ -1533,4 +1533,28 @@ anz der Bedingungen ist erforderlich Alle auswählen %1$d%% Verfügbarkeit Namecoin-Einstellungen + Wallet + Guthaben + Senden + Empfangen + Transaktionen + Keine Wallet verbunden + Richte eine Nostr Wallet Connect (NWC)-Verbindung in deinen Zap-Einstellungen ein, um die Wallet zu nutzen. + Wallet einrichten + Sats + BOLT-11-Rechnung einfügen + Bezahlen + Zahlung erfolgreich + Zahlung wird gesendet… + Betrag (Sats) + Beschreibung (optional) + Rechnung erstellen + Rechnung wird erstellt… + Rechnung kopieren + Noch keine Transaktionen + Wird geladen… + Empfangen + Gesendet + Aktualisieren + %1$s → %2$s diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 4ab3d5decb..ef5a066e79 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1505,7 +1505,7 @@ Importar lista de seguidos Selecionar usuários para seguir Perfil para importar - busca por nome, npub1…, alice@example.com + busca por nome, npub1…, alice@example.com Suporta npub, NIP-05, hex e Namecoin (.bit / d/ / id/) Buscar lista de seguidos Dica @@ -1529,4 +1529,28 @@ %1$d%% de disponibilidade Configurações do Namecoin Explorador Bitcoin (OTS) + Carteira + Saldo + Enviar + Receber + Transações + Nenhuma carteira conectada + Configure uma conexão Nostr Wallet Connect (NWC) nas configurações de zap para usar a carteira. + Configurar Carteira + sats + Cole uma fatura BOLT-11 + Pagar + Pagamento bem-sucedido + Enviando pagamento… + Valor (sats) + Descrição (opcional) + Criar Fatura + Criando fatura… + Copiar Fatura + Nenhuma transação ainda + Carregando… + Recebido + Enviado + Atualizar + %1$s → %2$s diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 60a129aa57..4040381c66 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1527,4 +1527,28 @@ Välj alla %1$d%% drifttid Namecoin-inställningar + Plånbok + Saldo + Skicka + Ta emot + Transaktioner + Ingen plånbok ansluten + Konfigurera en Nostr Wallet Connect (NWC)-anslutning i dina zap-inställningar för att använda plånboken. + Konfigurera plånbok + sats + Klistra in en BOLT-11-faktura + Betala + Betalning lyckades + Skickar betalning… + Belopp (sats) + Beskrivning (valfritt) + Skapa faktura + Skapar faktura… + Kopiera faktura + Inga transaktioner ännu + Laddar… + Mottagen + Skickad + Uppdatera + %1$s → %2$s From caee58da55e59aec9a6d3c06e787c7fba3803bc3 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sat, 14 Mar 2026 20:02:48 +0000 Subject: [PATCH 094/172] Add migrations.xml to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d87f7f1462..945f2e02a8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ /.idea/AndroidProjectSystem.xml /.idea/deviceManager.xml /.idea/inspectionProfiles/ +/.idea/migrations.xml /commons/.idea/gradle.xml /commons/.idea/misc.xml /commons/.idea/workspace.xml From 2df98c456adec2109618afe003c0f358d38f9590 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 14 Mar 2026 20:04:41 +0000 Subject: [PATCH 095/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 46 +++++++++---------- .../src/main/res/values-de-rDE/strings.xml | 26 +---------- .../src/main/res/values-pt-rBR/strings.xml | 26 +---------- .../src/main/res/values-sv-rSE/strings.xml | 46 +++++++++---------- 4 files changed, 46 insertions(+), 98 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 3cbc5e20df..54e559e8ca 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1047,6 +1047,28 @@ Globální Krátké Šachy + Peněženka + Zůstatek + Odeslat + Přijmout + Transakce + Žádná peněženka není připojena + Nastavte připojení Nostr Wallet Connect (NWC) v nastavení zapů, abyste mohli peněženku používat. + Nastavit peněženku + Vložte fakturu BOLT-11 + Zaplatit + Platba úspěšná + Odesílání platby… + Částka (sats) + Popis (volitelné) + Vytvořit fakturu + Vytváření faktury… + Kopírovat fakturu + Zatím žádné transakce + Načítání… + Přijato + Odesláno + Obnovit Bezpečnostní filtry Importovat sledované Nový příspěvek @@ -1529,28 +1551,4 @@ %1$d%% dostupnost Nastavení Namecoin Průzkumník Bitcoin (OTS) - Peněženka - Zůstatek - Odeslat - Přijmout - Transakce - Žádná peněženka není připojena - Nastavte připojení Nostr Wallet Connect (NWC) v nastavení zapů, abyste mohli peněženku používat. - Nastavit peněženku - sats - Vložte fakturu BOLT-11 - Zaplatit - Platba úspěšná - Odesílání platby… - Částka (sats) - Popis (volitelné) - Vytvořit fakturu - Vytváření faktury… - Kopírovat fakturu - Zatím žádné transakce - Načítání… - Přijato - Odesláno - Obnovit - %1$s → %2$s diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 477dd288cf..a1df34e69a 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1510,7 +1510,7 @@ anz der Bedingungen ist erforderlich Folgeliste importieren Benutzer zum Folgen auswählen Profil, aus dem importiert werden soll - Namenssuche, npub1…, alice@example.com + Namenssuche, npub1…, alice@example.com Unterstützt npub, NIP-05, Hex und Namecoin (.bit / d/ / id/) Folgeliste nachschlagen Tipp @@ -1533,28 +1533,4 @@ anz der Bedingungen ist erforderlich Alle auswählen %1$d%% Verfügbarkeit Namecoin-Einstellungen - Wallet - Guthaben - Senden - Empfangen - Transaktionen - Keine Wallet verbunden - Richte eine Nostr Wallet Connect (NWC)-Verbindung in deinen Zap-Einstellungen ein, um die Wallet zu nutzen. - Wallet einrichten - Sats - BOLT-11-Rechnung einfügen - Bezahlen - Zahlung erfolgreich - Zahlung wird gesendet… - Betrag (Sats) - Beschreibung (optional) - Rechnung erstellen - Rechnung wird erstellt… - Rechnung kopieren - Noch keine Transaktionen - Wird geladen… - Empfangen - Gesendet - Aktualisieren - %1$s → %2$s diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index ef5a066e79..4ab3d5decb 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1505,7 +1505,7 @@ Importar lista de seguidos Selecionar usuários para seguir Perfil para importar - busca por nome, npub1…, alice@example.com + busca por nome, npub1…, alice@example.com Suporta npub, NIP-05, hex e Namecoin (.bit / d/ / id/) Buscar lista de seguidos Dica @@ -1529,28 +1529,4 @@ %1$d%% de disponibilidade Configurações do Namecoin Explorador Bitcoin (OTS) - Carteira - Saldo - Enviar - Receber - Transações - Nenhuma carteira conectada - Configure uma conexão Nostr Wallet Connect (NWC) nas configurações de zap para usar a carteira. - Configurar Carteira - sats - Cole uma fatura BOLT-11 - Pagar - Pagamento bem-sucedido - Enviando pagamento… - Valor (sats) - Descrição (opcional) - Criar Fatura - Criando fatura… - Copiar Fatura - Nenhuma transação ainda - Carregando… - Recebido - Enviado - Atualizar - %1$s → %2$s diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 4040381c66..775a0a0f5f 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1046,6 +1046,28 @@ Globalt Kortfilmer Schack + Plånbok + Saldo + Skicka + Ta emot + Transaktioner + Ingen plånbok ansluten + Konfigurera en Nostr Wallet Connect (NWC)-anslutning i dina zap-inställningar för att använda plånboken. + Konfigurera plånbok + Klistra in en BOLT-11-faktura + Betala + Betalning lyckades + Skickar betalning… + Belopp (sats) + Beskrivning (valfritt) + Skapa faktura + Skapar faktura… + Kopiera faktura + Inga transaktioner ännu + Laddar… + Mottagen + Skickad + Uppdatera Säkerhetsfilter Importera följare Nytt inlägg @@ -1527,28 +1549,4 @@ Välj alla %1$d%% drifttid Namecoin-inställningar - Plånbok - Saldo - Skicka - Ta emot - Transaktioner - Ingen plånbok ansluten - Konfigurera en Nostr Wallet Connect (NWC)-anslutning i dina zap-inställningar för att använda plånboken. - Konfigurera plånbok - sats - Klistra in en BOLT-11-faktura - Betala - Betalning lyckades - Skickar betalning… - Belopp (sats) - Beskrivning (valfritt) - Skapa faktura - Skapar faktura… - Kopiera faktura - Inga transaktioner ännu - Laddar… - Mottagen - Skickad - Uppdatera - %1$s → %2$s From 00fb924dbb6a344a0c3f5814cb55b4650a8a3571 Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sun, 15 Mar 2026 11:50:24 +0000 Subject: [PATCH 096/172] Update README with build instructions and requirements Added instructions for full build and requirements. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index d1d9dbe7ba..a19f165504 100644 --- a/README.md +++ b/README.md @@ -218,6 +218,13 @@ Build and run the Desktop app (requires Java 21+): ```bash ./gradlew :desktopApp:run ``` +Full build (including tests) +```bash +./gradlew build +``` +Requirements: +- Xcode and iOS simulator +- libsodium installed (e.g. via brew: `brew install libsodium` ## Testing ```bash From 79990103908cf73538f9712256a87f9706dc3f9c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 15 Mar 2026 11:51:55 +0000 Subject: [PATCH 097/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 2 ++ .../src/main/res/values-de-rDE/strings.xml | 25 +++++++++++++++++++ .../src/main/res/values-hi-rIN/strings.xml | 16 ++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 24 ++++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 3 +++ 5 files changed, 70 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 54e559e8ca..fa6683bf49 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1055,6 +1055,7 @@ Žádná peněženka není připojena Nastavte připojení Nostr Wallet Connect (NWC) v nastavení zapů, abyste mohli peněženku používat. Nastavit peněženku + sats Vložte fakturu BOLT-11 Zaplatit Platba úspěšná @@ -1304,6 +1305,7 @@ Vyberte jazyk, do kterého chcete obsah přeložit. Předvolby zobrazení jazyka Pro každý přeložený jazykový pár zvolte, který jazyk zobrazit jako první. + %1$s → %2$s Hledat jazyky Přidat jazyk Přidat jazykový pár diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index a1df34e69a..cfe786197a 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1052,6 +1052,29 @@ anz der Bedingungen ist erforderlich Global Kurzfilme Schach + Wallet + Guthaben + Senden + Empfangen + Transaktionen + Keine Wallet verbunden + Richte eine Nostr Wallet Connect (NWC)-Verbindung in deinen Zap-Einstellungen ein, um die Wallet zu nutzen. + Wallet einrichten + Sats + BOLT-11-Rechnung einfügen + Bezahlen + Zahlung erfolgreich + Zahlung wird gesendet… + Betrag (Sats) + Beschreibung (optional) + Rechnung erstellen + Rechnung wird erstellt… + Rechnung kopieren + Noch keine Transaktionen + Wird geladen… + Empfangen + Gesendet + Aktualisieren Sicherheitsfilter Folgeliste importieren Neuer Beitrag @@ -1287,6 +1310,7 @@ anz der Bedingungen ist erforderlich Wähle die Sprache, in die der Inhalt übersetzt werden soll. Sprachanzeigeeinstellungen Wähle für jedes übersetzte Sprachpaar, welche Sprache zuerst angezeigt werden soll. + %1$s → %2$s Sprachen suchen Sprache hinzufügen Sprachpaar hinzufügen @@ -1533,4 +1557,5 @@ anz der Bedingungen ist erforderlich Alle auswählen %1$d%% Verfügbarkeit Namecoin-Einstellungen + Bitcoin Explorer (OTS) diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index be1bd8761e..6332634f02 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -1052,6 +1052,22 @@ वैश्विक छोटे चतुरंग + धनकोष + शेष + भेजें + प्राप्त करें + लेनदेन + कोई धनकोष संयोजित नहीं + एक नोस्टर धनकोष संयोजन (एनडबल्यूसी॰) स्थापित करें आपके ज्साप स्थापना विकल्पों में धनकोष का उपयोग करने के लिए। + धनकोष की स्थापना करें + साट्स + बोल्ट॰-११ चालान चिपकाएँ + भुगतान करें + भुगतान सफल + भुगतान भेजा जा रहा है… + संख्या (साट्स) + विवरण (विकल्पात्मक) + चालान बनाएँ सुरक्षार्थ छलनियाँ अनुचरित आयात करें नया पत्र प्रकाशन diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 4ab3d5decb..3816f9dfbf 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1047,6 +1047,29 @@ Global Vídeos Curtos Xadrez + Carteira + Saldo + Enviar + Receber + Transações + Nenhuma carteira conectada + Configure uma conexão Nostr Wallet Connect (NWC) nas configurações de zap para usar a carteira. + Configurar Carteira + sats + Cole uma fatura BOLT-11 + Pagar + Pagamento bem-sucedido + Enviando pagamento… + Valor (sats) + Descrição (opcional) + Criar Fatura + Criando fatura… + Copiar Fatura + Nenhuma transação ainda + Carregando… + Recebido + Enviado + Atualizar Filtros de Segurança Importar Seguidos Novo Post @@ -1282,6 +1305,7 @@ Escolha o idioma para o qual o conteúdo será traduzido. Preferências de exibição de idioma Para cada par de idiomas traduzido, escolha qual idioma exibir primeiro. + %1$s → %2$s Buscar idiomas Adicionar idioma Adicionar par de idiomas diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 775a0a0f5f..d48eddc078 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1054,6 +1054,7 @@ Ingen plånbok ansluten Konfigurera en Nostr Wallet Connect (NWC)-anslutning i dina zap-inställningar för att använda plånboken. Konfigurera plånbok + sats Klistra in en BOLT-11-faktura Betala Betalning lyckades @@ -1303,6 +1304,7 @@ Välj det språk du vill översätta innehållet till. Språkvisningsinställningar Välj för varje översatt språkpar vilket språk som ska visas först. + %1$s → %2$s Sök språk Lägg till språk Lägg till språkpar @@ -1549,4 +1551,5 @@ Välj alla %1$d%% drifttid Namecoin-inställningar + Bitcoin Explorer (OTS) From b1723a5dfb68c6994dc7105f27e9192ba87deebf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 14:41:19 +0000 Subject: [PATCH 098/172] feat: add relay event count stats to AllRelay settings screen Use NostrClient's NIP-45 COUNT queries to show how many events each relay stores, with filters specific to each relay role: - Outbox: events authored by the user - Inbox: events where the user is p-tagged - DM Inbox: DM events (kind 4, 1059) tagging the user - Private Home: events authored by the user - Proxy: total event count - Search: total event count - Indexer: kind 0 and kind 10002 counts separately - Broadcast: no count (as specified) https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA --- .../loggedIn/relays/AllRelayListScreen.kt | 102 ++++++++- .../common/BasicRelaySetupInfoClickableRow.kt | 6 + .../common/BasicRelaySetupInfoDialog.kt | 2 + .../relays/common/RelayEventCountRow.kt | 85 +++++++ .../relays/common/RelayEventCountViewModel.kt | 214 ++++++++++++++++++ .../loggedIn/relays/dm/DMRelayListView.kt | 4 + .../relays/indexer/IndexerRelayListView.kt | 4 + .../nip37/PrivateOutboxRelayListView.kt | 4 + .../relays/nip65/Nip65RelayListView.kt | 6 + .../relays/proxy/ProxyRelayListView.kt | 4 + .../relays/search/SearchRelayListView.kt | 4 + amethyst/src/main/res/values/strings.xml | 1 + 12 files changed, 429 insertions(+), 7 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index e2b79d62cb..a9b79bb717 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelay import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayEventCountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter @@ -109,6 +110,13 @@ fun AllRelayListScreen( val indexerViewModel: IndexerRelayListViewModel = viewModel() val proxyViewModel: ProxyRelayListViewModel = viewModel() val relayFeedsViewModel: RelayFeedsListViewModel = viewModel() + val outboxCountViewModel: RelayEventCountViewModel = viewModel(key = "outboxCount") + val inboxCountViewModel: RelayEventCountViewModel = viewModel(key = "inboxCount") + val dmCountViewModel: RelayEventCountViewModel = viewModel(key = "dmCount") + val privateHomeCountViewModel: RelayEventCountViewModel = viewModel(key = "privateHomeCount") + val proxyCountViewModel: RelayEventCountViewModel = viewModel(key = "proxyCount") + val indexerCountViewModel: RelayEventCountViewModel = viewModel(key = "indexerCount") + val searchCountViewModel: RelayEventCountViewModel = viewModel(key = "searchCount") dmViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel) @@ -151,6 +159,13 @@ fun AllRelayListScreen( indexerViewModel, proxyViewModel, relayFeedsViewModel, + outboxCountViewModel, + inboxCountViewModel, + dmCountViewModel, + privateHomeCountViewModel, + proxyCountViewModel, + indexerCountViewModel, + searchCountViewModel, accountViewModel, nav, ) @@ -171,6 +186,13 @@ fun MappedAllRelayListView( indexerViewModel: IndexerRelayListViewModel, proxyViewModel: ProxyRelayListViewModel, relayFeedsViewModel: RelayFeedsListViewModel, + outboxCountViewModel: RelayEventCountViewModel, + inboxCountViewModel: RelayEventCountViewModel, + dmCountViewModel: RelayEventCountViewModel, + privateHomeCountViewModel: RelayEventCountViewModel, + proxyCountViewModel: RelayEventCountViewModel, + indexerCountViewModel: RelayEventCountViewModel, + searchCountViewModel: RelayEventCountViewModel, accountViewModel: AccountViewModel, nav: INav, ) { @@ -188,6 +210,72 @@ fun MappedAllRelayListView( val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle() + val outboxCounts by outboxCountViewModel.counts.collectAsStateWithLifecycle() + val inboxCounts by inboxCountViewModel.counts.collectAsStateWithLifecycle() + val dmCounts by dmCountViewModel.counts.collectAsStateWithLifecycle() + val privateHomeCounts by privateHomeCountViewModel.counts.collectAsStateWithLifecycle() + val proxyCounts by proxyCountViewModel.counts.collectAsStateWithLifecycle() + val indexerCounts by indexerCountViewModel.counts.collectAsStateWithLifecycle() + val searchCounts by searchCountViewModel.counts.collectAsStateWithLifecycle() + + val userPubKey = accountViewModel.account.pubKey + + LaunchedEffect(homeFeedState) { + if (homeFeedState.isNotEmpty()) { + outboxCountViewModel.queryCountsForRelays( + RelayEventCountViewModel.authorCountFilters(userPubKey, homeFeedState.map { it.relay }), + ) + } + } + + LaunchedEffect(notifFeedState) { + if (notifFeedState.isNotEmpty()) { + inboxCountViewModel.queryCountsForRelays( + RelayEventCountViewModel.pTagCountFilters(userPubKey, notifFeedState.map { it.relay }), + ) + } + } + + LaunchedEffect(dmFeedState) { + if (dmFeedState.isNotEmpty()) { + dmCountViewModel.queryCountsForRelays( + RelayEventCountViewModel.dmCountFilters(userPubKey, dmFeedState.map { it.relay }), + ) + } + } + + LaunchedEffect(privateOutboxFeedState) { + if (privateOutboxFeedState.isNotEmpty()) { + privateHomeCountViewModel.queryCountsForRelays( + RelayEventCountViewModel.authorCountFilters(userPubKey, privateOutboxFeedState.map { it.relay }), + ) + } + } + + LaunchedEffect(proxyRelays) { + if (proxyRelays.isNotEmpty()) { + proxyCountViewModel.queryCountsForRelays( + RelayEventCountViewModel.totalCountFilters(proxyRelays.map { it.relay }), + ) + } + } + + LaunchedEffect(indexerRelays) { + if (indexerRelays.isNotEmpty()) { + indexerCountViewModel.queryCountsForRelays( + RelayEventCountViewModel.indexerCountFilters(indexerRelays.map { it.relay }), + ) + } + } + + LaunchedEffect(searchFeedState) { + if (searchFeedState.isNotEmpty()) { + searchCountViewModel.queryCountsForRelays( + RelayEventCountViewModel.totalCountFilters(searchFeedState.map { it.relay }), + ) + } + } + Scaffold( topBar = { SavingTopBar( @@ -261,7 +349,7 @@ fun MappedAllRelayListView( SettingsCategoryFirstModifier, ) } - renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav) + renderNip65HomeItems(homeFeedState, nip65ViewModel, accountViewModel, nav, outboxCounts) item { SettingsCategory( @@ -270,7 +358,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav) + renderNip65NotifItems(notifFeedState, nip65ViewModel, accountViewModel, nav, inboxCounts) item { SettingsCategoryWithButton( @@ -282,7 +370,7 @@ fun MappedAllRelayListView( }, ) } - renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav) + renderDMItems(dmFeedState, dmViewModel, accountViewModel, nav, dmCounts) item { SettingsCategory( @@ -291,7 +379,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav) + renderPrivateOutboxItems(privateOutboxFeedState, privateOutboxViewModel, accountViewModel, nav, privateHomeCounts) item { SettingsCategory( @@ -300,7 +388,7 @@ fun MappedAllRelayListView( SettingsCategorySpacingModifier, ) } - renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav) + renderProxyItems(proxyRelays, proxyViewModel, accountViewModel, nav, proxyCounts) item { SettingsCategory( @@ -320,7 +408,7 @@ fun MappedAllRelayListView( ResetIndexerRelays(indexerViewModel) } } - renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav) + renderIndexerItems(indexerRelays, indexerViewModel, accountViewModel, nav, indexerCounts) item { SettingsCategoryWithButton( @@ -331,7 +419,7 @@ fun MappedAllRelayListView( ResetSearchRelays(searchViewModel) } } - renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav) + renderSearchItems(searchFeedState, searchViewModel, accountViewModel, nav, searchCounts) item { SettingsCategory( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt index 73fad951f1..625fde7c29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoClickableRow.kt @@ -63,6 +63,7 @@ fun BasicRelaySetupInfoClickableRow( onClick: () -> Unit, nip11CachedRetriever: Nip11CachedRetriever, modifier: Modifier = Modifier, + countResult: RelayCountResult? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -104,6 +105,11 @@ fun BasicRelaySetupInfoClickableRow( UsedBy(item, accountViewModel, nav) + RelayEventCountRow( + countResult = countResult, + modifier = ReactionRowHeightChatMaxWidth, + ) + RelayStatusRow( item = item, onClick = onClick, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 1a85e2aec2..3196a11747 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -32,6 +32,7 @@ fun BasicRelaySetupInfoDialog( item: BasicRelaySetupInfo, nip11CachedRetriever: Nip11CachedRetriever, onDelete: ((BasicRelaySetupInfo) -> Unit)?, + countResult: RelayCountResult? = null, accountViewModel: AccountViewModel, nav: INav, ) { @@ -43,6 +44,7 @@ fun BasicRelaySetupInfoDialog( onClick = { nav.nav(Route.RelayInfo(item.relay.url)) }, nip11CachedRetriever = nip11CachedRetriever, modifier = HalfVertPadding, + countResult = countResult, accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt new file mode 100644 index 0000000000..c1858eb9d7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Storage +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.countToHumanReadable +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Font12SP +import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier +import com.vitorpamplona.amethyst.ui.theme.allGoodColor +import com.vitorpamplona.amethyst.ui.theme.placeholderText + +@Composable +fun RelayEventCountRow( + countResult: RelayCountResult?, + modifier: Modifier, +) { + if (countResult == null || countResult.counts.isEmpty()) return + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = modifier, + ) { + Icon( + imageVector = Icons.Default.Storage, + contentDescription = stringRes(R.string.relay_event_count), + modifier = Size15Modifier, + tint = MaterialTheme.colorScheme.allGoodColor, + ) + + countResult.counts.forEachIndexed { index, entry -> + if (index > 0) { + Spacer(modifier = Modifier.width(8.dp)) + } + + val text = + if (entry.approximate) { + "~${countToHumanReadable(entry.count, entry.label)}" + } else { + countToHumanReadable(entry.count, entry.label) + } + + Text( + text = text, + maxLines = 1, + fontSize = Font12SP, + modifier = HalfStartPadding, + color = MaterialTheme.colorScheme.placeholderText, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt new file mode 100644 index 0000000000..5529c2d920 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common + +import androidx.compose.runtime.Immutable +import androidx.lifecycle.ViewModel +import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +@Immutable +data class RelayCountResult( + val counts: List = emptyList(), +) { + @Immutable + data class CountEntry( + val label: String, + val count: Int, + val approximate: Boolean = false, + ) +} + +class RelayEventCountViewModel : ViewModel(), IRelayClientListener { + private val client: INostrClient get() = Amethyst.instance.client + + private val _counts = MutableStateFlow>(emptyMap()) + val counts = _counts.asStateFlow() + + // Maps subId -> (relay, filterIndex) so we can route count results + private val subIdToRelay = mutableMapOf>() + // Maps relay -> list of (subId, label) to track active queries + private val relayQueries = mutableMapOf>() + + private data class QueryInfo( + val subId: String, + val label: String, + val filterIndex: Int, + ) + + fun queryCountsForRelays(queries: Map>) { + cleanup() + client.subscribe(this) + + queries.forEach { (relay, filters) -> + val queryInfos = mutableListOf() + + filters.forEachIndexed { index, countFilter -> + val subId = newSubId() + subIdToRelay[subId] = Pair(relay, index) + queryInfos.add(QueryInfo(subId, countFilter.label, index)) + + client.queryCount( + subId = subId, + filters = mapOf(relay to listOf(countFilter.filter)), + ) + } + + relayQueries[relay] = queryInfos + } + } + + override fun onIncomingMessage( + relay: IRelayClient, + msgStr: String, + msg: Message, + ) { + if (msg is CountMessage) { + val (relayUrl, filterIndex) = subIdToRelay[msg.queryId] ?: return + val queryInfos = relayQueries[relayUrl] ?: return + val queryInfo = queryInfos.find { it.filterIndex == filterIndex } ?: return + + _counts.update { currentMap -> + val currentResult = currentMap[relayUrl] ?: RelayCountResult() + val updatedEntries = currentResult.counts.toMutableList() + + // Replace or add the entry for this filter + val existingIndex = updatedEntries.indexOfFirst { it.label == queryInfo.label } + val newEntry = + RelayCountResult.CountEntry( + label = queryInfo.label, + count = msg.result.count, + approximate = msg.result.approximate, + ) + + if (existingIndex >= 0) { + updatedEntries[existingIndex] = newEntry + } else { + updatedEntries.add(newEntry) + } + + currentMap + (relayUrl to RelayCountResult(updatedEntries)) + } + } + } + + private fun cleanup() { + // Close existing queries + subIdToRelay.keys.forEach { subId -> + client.close(subId) + } + subIdToRelay.clear() + relayQueries.clear() + _counts.value = emptyMap() + client.unsubscribe(this) + } + + override fun onCleared() { + cleanup() + super.onCleared() + } + + companion object { + fun authorCountFilters( + userPubKey: HexKey, + relays: List, + ): Map> = + relays.associateWith { + listOf( + CountFilter( + label = "events", + filter = Filter(authors = listOf(userPubKey)), + ), + ) + } + + fun pTagCountFilters( + userPubKey: HexKey, + relays: List, + ): Map> = + relays.associateWith { + listOf( + CountFilter( + label = "events", + filter = Filter(tags = mapOf("p" to listOf(userPubKey))), + ), + ) + } + + fun dmCountFilters( + userPubKey: HexKey, + relays: List, + ): Map> = + relays.associateWith { + listOf( + CountFilter( + label = "events", + filter = + Filter( + kinds = listOf(GiftWrapEvent.KIND, PrivateDmEvent.KIND), + tags = mapOf("p" to listOf(userPubKey)), + ), + ), + ) + } + + fun totalCountFilters(relays: List): Map> = + relays.associateWith { + listOf( + CountFilter( + label = "events", + filter = Filter(kinds = null), + ), + ) + } + + fun indexerCountFilters(relays: List): Map> = + relays.associateWith { + listOf( + CountFilter( + label = "kind 0", + filter = Filter(kinds = listOf(0)), + ), + CountFilter( + label = "kind 10002", + filter = Filter(kinds = listOf(10002)), + ), + ) + } + } +} + +data class CountFilter( + val label: String, + val filter: Filter, +) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt index 37e02b99be..f6e9395714 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListView.kt @@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable fun DMRelayList( @@ -64,12 +66,14 @@ fun LazyListScope.renderDMItems( postViewModel: DMRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + countResults: Map = emptyMap(), ) { itemsIndexed(feedState, key = { _, item -> "DM" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt index 1af2490cdf..56dbf8018c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListView.kt @@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable fun IndexerRelayList( @@ -65,12 +67,14 @@ fun LazyListScope.renderIndexerItems( postViewModel: IndexerRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + countResults: Map = emptyMap(), ) { itemsIndexed(feedState, key = { _, item -> "Indexer" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt index 212e1b3b50..a372d7286b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListView.kt @@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable fun PrivateOutboxRelayList( @@ -64,12 +66,14 @@ fun LazyListScope.renderPrivateOutboxItems( postViewModel: PrivateOutboxRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + countResults: Map = emptyMap(), ) { itemsIndexed(feedState, key = { _, item -> "Outbox" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt index 7ac2497f0c..73c201b6bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListView.kt @@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable fun Nip65RelayList( @@ -107,12 +109,14 @@ fun LazyListScope.renderNip65HomeItems( postViewModel: Nip65RelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + countResults: Map = emptyMap(), ) { itemsIndexed(feedState, key = { _, item -> "Nip65Home" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteHomeRelay(item) }, nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], accountViewModel = accountViewModel, nav = nav, ) @@ -133,12 +137,14 @@ fun LazyListScope.renderNip65NotifItems( postViewModel: Nip65RelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + countResults: Map = emptyMap(), ) { itemsIndexed(feedState, key = { _, item -> "Nip65Notif" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteNotifRelay(item) }, nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt index 2997a5c398..a737641f2f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListView.kt @@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable fun ProxyRelayList( @@ -65,12 +67,14 @@ fun LazyListScope.renderProxyItems( postViewModel: ProxyRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + countResults: Map = emptyMap(), ) { itemsIndexed(feedState, key = { _, item -> "Proxy" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt index 15f6975960..1077d42e2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListView.kt @@ -35,10 +35,12 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.rememberExtendedNav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Composable fun SearchRelayList( @@ -65,12 +67,14 @@ fun LazyListScope.renderSearchItems( postViewModel: SearchRelayListViewModel, accountViewModel: AccountViewModel, nav: INav, + countResults: Map = emptyMap(), ) { itemsIndexed(feedState, key = { _, item -> "Search" + item.relay.url }) { index, item -> BasicRelaySetupInfoDialog( item, onDelete = { postViewModel.deleteRelay(item) }, nip11CachedRetriever = Amethyst.instance.nip11Cache, + countResult = countResults[item.relay], accountViewModel = accountViewModel, nav = nav, ) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 36ce301422..e322c05e7e 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -776,6 +776,7 @@ Write to Relay The amount in bytes that was sent to this relay, including filters and events The amount in bytes that was received from this relay, including filters and events + Events stored An error occurred trying to get relay information from %1$s Owner Used By From 764c2241cae17c65939de0395f064fbe1a422153 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 15:02:09 +0000 Subject: [PATCH 099/172] refactor: integrate relay event counts into each ViewModel Move count query logic from separate RelayEventCountViewModel into each relay list's own ViewModel. BasicRelaySetupInfoModel now manages counts via countFilters() override. Nip65RelayListViewModel gets its own count infrastructure for home (outbox) and notification (inbox) relay lists. Simplify AllRelayListScreen by removing 7 extra count ViewModels and their LaunchedEffects. https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA --- .../loggedIn/relays/AllRelayListScreen.kt | 94 +--------- .../relays/common/BasicRelaySetupInfoModel.kt | 104 ++++++++++- .../relays/common/RelayEventCountViewModel.kt | 173 ------------------ .../relays/dm/DMRelayListViewModel.kt | 16 ++ .../indexer/IndexerRelayListViewModel.kt | 14 ++ .../nip37/PrivateOutboxRelayListViewModel.kt | 10 + .../relays/nip65/Nip65RelayListViewModel.kt | 92 +++++++++- .../relays/proxy/ProxyRelayListViewModel.kt | 10 + .../relays/search/SearchRelayListViewModel.kt | 10 + 9 files changed, 261 insertions(+), 262 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt index a9b79bb717..e45562fe9b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/AllRelayListScreen.kt @@ -60,7 +60,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.BlockedRelay import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.blocked.renderBlockedItems import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.BroadcastRelayListViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.broadcast.renderBroadcastItems -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayEventCountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayExporter import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayListCollection import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayZipExporter @@ -110,13 +109,6 @@ fun AllRelayListScreen( val indexerViewModel: IndexerRelayListViewModel = viewModel() val proxyViewModel: ProxyRelayListViewModel = viewModel() val relayFeedsViewModel: RelayFeedsListViewModel = viewModel() - val outboxCountViewModel: RelayEventCountViewModel = viewModel(key = "outboxCount") - val inboxCountViewModel: RelayEventCountViewModel = viewModel(key = "inboxCount") - val dmCountViewModel: RelayEventCountViewModel = viewModel(key = "dmCount") - val privateHomeCountViewModel: RelayEventCountViewModel = viewModel(key = "privateHomeCount") - val proxyCountViewModel: RelayEventCountViewModel = viewModel(key = "proxyCount") - val indexerCountViewModel: RelayEventCountViewModel = viewModel(key = "indexerCount") - val searchCountViewModel: RelayEventCountViewModel = viewModel(key = "searchCount") dmViewModel.init(accountViewModel) nip65ViewModel.init(accountViewModel) @@ -159,13 +151,6 @@ fun AllRelayListScreen( indexerViewModel, proxyViewModel, relayFeedsViewModel, - outboxCountViewModel, - inboxCountViewModel, - dmCountViewModel, - privateHomeCountViewModel, - proxyCountViewModel, - indexerCountViewModel, - searchCountViewModel, accountViewModel, nav, ) @@ -186,13 +171,6 @@ fun MappedAllRelayListView( indexerViewModel: IndexerRelayListViewModel, proxyViewModel: ProxyRelayListViewModel, relayFeedsViewModel: RelayFeedsListViewModel, - outboxCountViewModel: RelayEventCountViewModel, - inboxCountViewModel: RelayEventCountViewModel, - dmCountViewModel: RelayEventCountViewModel, - privateHomeCountViewModel: RelayEventCountViewModel, - proxyCountViewModel: RelayEventCountViewModel, - indexerCountViewModel: RelayEventCountViewModel, - searchCountViewModel: RelayEventCountViewModel, accountViewModel: AccountViewModel, nav: INav, ) { @@ -210,71 +188,13 @@ fun MappedAllRelayListView( val proxyRelays by proxyViewModel.relays.collectAsStateWithLifecycle() val relayFeedsFeedState by relayFeedsViewModel.relays.collectAsStateWithLifecycle() - val outboxCounts by outboxCountViewModel.counts.collectAsStateWithLifecycle() - val inboxCounts by inboxCountViewModel.counts.collectAsStateWithLifecycle() - val dmCounts by dmCountViewModel.counts.collectAsStateWithLifecycle() - val privateHomeCounts by privateHomeCountViewModel.counts.collectAsStateWithLifecycle() - val proxyCounts by proxyCountViewModel.counts.collectAsStateWithLifecycle() - val indexerCounts by indexerCountViewModel.counts.collectAsStateWithLifecycle() - val searchCounts by searchCountViewModel.counts.collectAsStateWithLifecycle() - - val userPubKey = accountViewModel.account.pubKey - - LaunchedEffect(homeFeedState) { - if (homeFeedState.isNotEmpty()) { - outboxCountViewModel.queryCountsForRelays( - RelayEventCountViewModel.authorCountFilters(userPubKey, homeFeedState.map { it.relay }), - ) - } - } - - LaunchedEffect(notifFeedState) { - if (notifFeedState.isNotEmpty()) { - inboxCountViewModel.queryCountsForRelays( - RelayEventCountViewModel.pTagCountFilters(userPubKey, notifFeedState.map { it.relay }), - ) - } - } - - LaunchedEffect(dmFeedState) { - if (dmFeedState.isNotEmpty()) { - dmCountViewModel.queryCountsForRelays( - RelayEventCountViewModel.dmCountFilters(userPubKey, dmFeedState.map { it.relay }), - ) - } - } - - LaunchedEffect(privateOutboxFeedState) { - if (privateOutboxFeedState.isNotEmpty()) { - privateHomeCountViewModel.queryCountsForRelays( - RelayEventCountViewModel.authorCountFilters(userPubKey, privateOutboxFeedState.map { it.relay }), - ) - } - } - - LaunchedEffect(proxyRelays) { - if (proxyRelays.isNotEmpty()) { - proxyCountViewModel.queryCountsForRelays( - RelayEventCountViewModel.totalCountFilters(proxyRelays.map { it.relay }), - ) - } - } - - LaunchedEffect(indexerRelays) { - if (indexerRelays.isNotEmpty()) { - indexerCountViewModel.queryCountsForRelays( - RelayEventCountViewModel.indexerCountFilters(indexerRelays.map { it.relay }), - ) - } - } - - LaunchedEffect(searchFeedState) { - if (searchFeedState.isNotEmpty()) { - searchCountViewModel.queryCountsForRelays( - RelayEventCountViewModel.totalCountFilters(searchFeedState.map { it.relay }), - ) - } - } + val outboxCounts by nip65ViewModel.homeCountResults.collectAsStateWithLifecycle() + val inboxCounts by nip65ViewModel.notifCountResults.collectAsStateWithLifecycle() + val dmCounts by dmViewModel.countResults.collectAsStateWithLifecycle() + val privateHomeCounts by privateOutboxViewModel.countResults.collectAsStateWithLifecycle() + val proxyCounts by proxyViewModel.countResults.collectAsStateWithLifecycle() + val indexerCounts by indexerViewModel.countResults.collectAsStateWithLifecycle() + val searchCounts by searchViewModel.countResults.collectAsStateWithLifecycle() Scaffold( topBar = { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 758c39646f..466cc16820 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -26,6 +26,11 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -33,13 +38,19 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -abstract class BasicRelaySetupInfoModel : ViewModel() { +abstract class BasicRelaySetupInfoModel : ViewModel(), IRelayClientListener { lateinit var accountViewModel: AccountViewModel lateinit var account: Account private val _relays = MutableStateFlow>(emptyList()) val relays = _relays.asStateFlow() + private val _countResults = MutableStateFlow>(emptyMap()) + val countResults = _countResults.asStateFlow() + + private val subIdToRelay = mutableMapOf>() + private val relayQueryInfos = mutableMapOf>() + var hasModified = false fun init(accountViewModel: AccountViewModel) { @@ -50,12 +61,15 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { fun load() { clear() loadRelayDocuments() + loadCounts() } abstract fun getRelayList(): List? abstract suspend fun saveRelayList(urlList: List) + open fun countFilters(relayUrl: NormalizedRelayUrl): List = emptyList() + fun create() { if (hasModified) { accountViewModel.launchSigner { @@ -79,6 +93,83 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { } } + private fun loadCounts() { + val client = Amethyst.instance.client + cleanupCounts() + + val relayList = _relays.value + if (relayList.isEmpty()) return + + val hasFilters = relayList.any { countFilters(it.relay).isNotEmpty() } + if (!hasFilters) return + + client.subscribe(this) + + relayList.forEach { item -> + val filters = countFilters(item.relay) + if (filters.isEmpty()) return@forEach + + val queryInfos = mutableListOf() + + filters.forEachIndexed { index, countFilter -> + val subId = newSubId() + subIdToRelay[subId] = Pair(item.relay, index) + queryInfos.add(CountQueryInfo(subId, countFilter.label, index)) + + client.queryCount( + subId = subId, + filters = mapOf(item.relay to listOf(countFilter.filter)), + ) + } + + relayQueryInfos[item.relay] = queryInfos + } + } + + override fun onIncomingMessage( + relay: IRelayClient, + msgStr: String, + msg: Message, + ) { + if (msg is CountMessage) { + val (relayUrl, filterIndex) = subIdToRelay[msg.queryId] ?: return + val queryInfos = relayQueryInfos[relayUrl] ?: return + val queryInfo = queryInfos.find { it.filterIndex == filterIndex } ?: return + + _countResults.update { currentMap -> + val currentResult = currentMap[relayUrl] ?: RelayCountResult() + val updatedEntries = currentResult.counts.toMutableList() + + val existingIndex = updatedEntries.indexOfFirst { it.label == queryInfo.label } + val newEntry = + RelayCountResult.CountEntry( + label = queryInfo.label, + count = msg.result.count, + approximate = msg.result.approximate, + ) + + if (existingIndex >= 0) { + updatedEntries[existingIndex] = newEntry + } else { + updatedEntries.add(newEntry) + } + + currentMap + (relayUrl to RelayCountResult(updatedEntries)) + } + } + } + + private fun cleanupCounts() { + val client = Amethyst.instance.client + subIdToRelay.keys.forEach { subId -> + client.close(subId) + } + subIdToRelay.clear() + relayQueryInfos.clear() + _countResults.value = emptyMap() + client.unsubscribe(this) + } + open fun relayListBuilder(): List { val relayList = getRelayList() ?: emptyList() @@ -122,4 +213,15 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { ) { _relays.update { it.replace(relay, relay.copy(paidRelay = paid)) } } + + override fun onCleared() { + cleanupCounts() + super.onCleared() + } + + private data class CountQueryInfo( + val subId: String, + val label: String, + val filterIndex: Int, + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt index 5529c2d920..6f77913fff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt @@ -21,21 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Immutable -import androidx.lifecycle.ViewModel -import com.vitorpamplona.amethyst.Amethyst -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener -import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient -import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId -import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage -import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter -import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update @Immutable data class RelayCountResult( @@ -49,165 +35,6 @@ data class RelayCountResult( ) } -class RelayEventCountViewModel : ViewModel(), IRelayClientListener { - private val client: INostrClient get() = Amethyst.instance.client - - private val _counts = MutableStateFlow>(emptyMap()) - val counts = _counts.asStateFlow() - - // Maps subId -> (relay, filterIndex) so we can route count results - private val subIdToRelay = mutableMapOf>() - // Maps relay -> list of (subId, label) to track active queries - private val relayQueries = mutableMapOf>() - - private data class QueryInfo( - val subId: String, - val label: String, - val filterIndex: Int, - ) - - fun queryCountsForRelays(queries: Map>) { - cleanup() - client.subscribe(this) - - queries.forEach { (relay, filters) -> - val queryInfos = mutableListOf() - - filters.forEachIndexed { index, countFilter -> - val subId = newSubId() - subIdToRelay[subId] = Pair(relay, index) - queryInfos.add(QueryInfo(subId, countFilter.label, index)) - - client.queryCount( - subId = subId, - filters = mapOf(relay to listOf(countFilter.filter)), - ) - } - - relayQueries[relay] = queryInfos - } - } - - override fun onIncomingMessage( - relay: IRelayClient, - msgStr: String, - msg: Message, - ) { - if (msg is CountMessage) { - val (relayUrl, filterIndex) = subIdToRelay[msg.queryId] ?: return - val queryInfos = relayQueries[relayUrl] ?: return - val queryInfo = queryInfos.find { it.filterIndex == filterIndex } ?: return - - _counts.update { currentMap -> - val currentResult = currentMap[relayUrl] ?: RelayCountResult() - val updatedEntries = currentResult.counts.toMutableList() - - // Replace or add the entry for this filter - val existingIndex = updatedEntries.indexOfFirst { it.label == queryInfo.label } - val newEntry = - RelayCountResult.CountEntry( - label = queryInfo.label, - count = msg.result.count, - approximate = msg.result.approximate, - ) - - if (existingIndex >= 0) { - updatedEntries[existingIndex] = newEntry - } else { - updatedEntries.add(newEntry) - } - - currentMap + (relayUrl to RelayCountResult(updatedEntries)) - } - } - } - - private fun cleanup() { - // Close existing queries - subIdToRelay.keys.forEach { subId -> - client.close(subId) - } - subIdToRelay.clear() - relayQueries.clear() - _counts.value = emptyMap() - client.unsubscribe(this) - } - - override fun onCleared() { - cleanup() - super.onCleared() - } - - companion object { - fun authorCountFilters( - userPubKey: HexKey, - relays: List, - ): Map> = - relays.associateWith { - listOf( - CountFilter( - label = "events", - filter = Filter(authors = listOf(userPubKey)), - ), - ) - } - - fun pTagCountFilters( - userPubKey: HexKey, - relays: List, - ): Map> = - relays.associateWith { - listOf( - CountFilter( - label = "events", - filter = Filter(tags = mapOf("p" to listOf(userPubKey))), - ), - ) - } - - fun dmCountFilters( - userPubKey: HexKey, - relays: List, - ): Map> = - relays.associateWith { - listOf( - CountFilter( - label = "events", - filter = - Filter( - kinds = listOf(GiftWrapEvent.KIND, PrivateDmEvent.KIND), - tags = mapOf("p" to listOf(userPubKey)), - ), - ), - ) - } - - fun totalCountFilters(relays: List): Map> = - relays.associateWith { - listOf( - CountFilter( - label = "events", - filter = Filter(kinds = null), - ), - ) - } - - fun indexerCountFilters(relays: List): Map> = - relays.associateWith { - listOf( - CountFilter( - label = "kind 0", - filter = Filter(kinds = listOf(0)), - ), - CountFilter( - label = "kind 10002", - filter = Filter(kinds = listOf(10002)), - ), - ) - } - } -} - data class CountFilter( val label: String, val filter: Filter, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt index d08612a485..a78df4519d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt @@ -22,7 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent @Stable class DMRelayListViewModel : BasicRelaySetupInfoModel() { @@ -31,4 +35,16 @@ class DMRelayListViewModel : BasicRelaySetupInfoModel() { override suspend fun saveRelayList(urlList: List) { account.saveDMRelayList(urlList) } + + override fun countFilters(relayUrl: NormalizedRelayUrl): List = + listOf( + CountFilter( + label = "events", + filter = + Filter( + kinds = listOf(GiftWrapEvent.KIND, PrivateDmEvent.KIND), + tags = mapOf("p" to listOf(account.pubKey)), + ), + ), + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt index 8379666245..c875c5a63f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable @@ -33,4 +35,16 @@ class IndexerRelayListViewModel : BasicRelaySetupInfoModel() { override suspend fun saveRelayList(urlList: List) { account.saveIndexerRelayList(urlList) } + + override fun countFilters(relayUrl: NormalizedRelayUrl): List = + listOf( + CountFilter( + label = "kind 0", + filter = Filter(kinds = listOf(0)), + ), + CountFilter( + label = "kind 10002", + filter = Filter(kinds = listOf(10002)), + ), + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt index a3e5f9f41f..5cd3e96ff9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37 import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable @@ -33,4 +35,12 @@ class PrivateOutboxRelayListViewModel : BasicRelaySetupInfoModel() { override suspend fun saveRelayList(urlList: List) { account.savePrivateOutboxRelayList(urlList) } + + override fun countFilters(relayUrl: NormalizedRelayUrl): List = + listOf( + CountFilter( + label = "events", + filter = Filter(authors = listOf(account.pubKey)), + ), + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index 134f7425c9..fa1427b892 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -28,7 +28,15 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayType import kotlinx.coroutines.Dispatchers @@ -38,7 +46,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @Stable -class Nip65RelayListViewModel : ViewModel() { +class Nip65RelayListViewModel : ViewModel(), IRelayClientListener { private lateinit var accountViewModel: AccountViewModel private lateinit var account: Account @@ -48,6 +56,14 @@ class Nip65RelayListViewModel : ViewModel() { private val _notificationRelays = MutableStateFlow>(emptyList()) val notificationRelays = _notificationRelays.asStateFlow() + private val _homeCountResults = MutableStateFlow>(emptyMap()) + val homeCountResults = _homeCountResults.asStateFlow() + + private val _notifCountResults = MutableStateFlow>(emptyMap()) + val notifCountResults = _notifCountResults.asStateFlow() + + private val subIdToRelay = mutableMapOf>() + var hasModified = false fun init(accountViewModel: AccountViewModel) { @@ -58,6 +74,7 @@ class Nip65RelayListViewModel : ViewModel() { fun load() { clear() loadRelayDocuments() + loadCounts() } fun create() { @@ -111,6 +128,74 @@ class Nip65RelayListViewModel : ViewModel() { } } + private fun loadCounts() { + val client = Amethyst.instance.client + cleanupCounts() + + val homeList = _homeRelays.value + val notifList = _notificationRelays.value + + if (homeList.isEmpty() && notifList.isEmpty()) return + + client.subscribe(this) + + homeList.forEach { item -> + val subId = newSubId() + subIdToRelay[subId] = Pair(item.relay, true) + client.queryCount( + subId = subId, + filters = mapOf(item.relay to listOf(Filter(authors = listOf(account.pubKey)))), + ) + } + + notifList.forEach { item -> + val subId = newSubId() + subIdToRelay[subId] = Pair(item.relay, false) + client.queryCount( + subId = subId, + filters = mapOf(item.relay to listOf(Filter(tags = mapOf("p" to listOf(account.pubKey))))), + ) + } + } + + override fun onIncomingMessage( + relay: IRelayClient, + msgStr: String, + msg: Message, + ) { + if (msg is CountMessage) { + val (relayUrl, isHome) = subIdToRelay[msg.queryId] ?: return + + val newResult = + RelayCountResult( + listOf( + RelayCountResult.CountEntry( + label = "events", + count = msg.result.count, + approximate = msg.result.approximate, + ), + ), + ) + + if (isHome) { + _homeCountResults.update { it + (relayUrl to newResult) } + } else { + _notifCountResults.update { it + (relayUrl to newResult) } + } + } + } + + private fun cleanupCounts() { + val client = Amethyst.instance.client + subIdToRelay.keys.forEach { subId -> + client.close(subId) + } + subIdToRelay.clear() + _homeCountResults.value = emptyMap() + _notifCountResults.value = emptyMap() + client.unsubscribe(this) + } + fun clear() { hasModified = false _homeRelays.update { @@ -181,4 +266,9 @@ class Nip65RelayListViewModel : ViewModel() { ) { _notificationRelays.update { it.replace(relay, relay.copy(paidRelay = paid)) } } + + override fun onCleared() { + cleanupCounts() + super.onCleared() + } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt index e2a0f13596..01d07c40b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable @@ -33,4 +35,12 @@ class ProxyRelayListViewModel : BasicRelaySetupInfoModel() { override suspend fun saveRelayList(urlList: List) { account.saveProxyRelayList(urlList) } + + override fun countFilters(relayUrl: NormalizedRelayUrl): List = + listOf( + CountFilter( + label = "events", + filter = Filter(), + ), + ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt index e2e3ba0aa0..aaf592a68b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt @@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable @@ -33,4 +35,12 @@ class SearchRelayListViewModel : BasicRelaySetupInfoModel() { override suspend fun saveRelayList(urlList: List) { account.saveSearchRelayList(urlList) } + + override fun countFilters(relayUrl: NormalizedRelayUrl): List = + listOf( + CountFilter( + label = "events", + filter = Filter(), + ), + ) } From 2c91fdea156be897d7a59fa3d457882a0bcb907e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 11:03:50 -0400 Subject: [PATCH 100/172] Avoids using `format` to write hex-encoded strings. --- .../domain/nip46/NostrConnectLoginUseCase.kt | 2 +- .../LnZapPaymentRequestEventTest.kt | 15 ++++++++------- .../namecoin/ElectrumXClient.kt | 3 ++- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/domain/nip46/NostrConnectLoginUseCase.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/domain/nip46/NostrConnectLoginUseCase.kt index 674a38219f..6725ef332f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/domain/nip46/NostrConnectLoginUseCase.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/domain/nip46/NostrConnectLoginUseCase.kt @@ -213,6 +213,6 @@ object NostrConnectLoginUseCase { private fun generateSecret(): String { val bytes = ByteArray(32) SecureRandom().nextBytes(bytes) - return bytes.joinToString("") { "%02x".format(it) } + return bytes.toHexKey() } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt index bc5af24320..271c2783b2 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip47WalletConnect import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import kotlinx.coroutines.test.runTest @@ -44,7 +45,7 @@ class LnZapPaymentRequestEventTest { val walletKeyPair = KeyPair() val clientSigner = NostrSignerInternal(clientKeyPair) val walletServicePubkey: HexKey = - walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + walletKeyPair.pubKey.toHexKey() val event = LnZapPaymentRequestEvent.create( @@ -68,7 +69,7 @@ class LnZapPaymentRequestEventTest { val walletKeyPair = KeyPair() val clientSigner = NostrSignerInternal(clientKeyPair) val walletServicePubkey: HexKey = - walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + walletKeyPair.pubKey.toHexKey() val request = GetBalanceMethod.create() val event = @@ -92,7 +93,7 @@ class LnZapPaymentRequestEventTest { val walletKeyPair = KeyPair() val clientSigner = NostrSignerInternal(clientKeyPair) val walletServicePubkey: HexKey = - walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + walletKeyPair.pubKey.toHexKey() val request = GetBalanceMethod.create() val event = @@ -116,7 +117,7 @@ class LnZapPaymentRequestEventTest { val clientSigner = NostrSignerInternal(clientKeyPair) val walletSigner = NostrSignerInternal(walletKeyPair) val walletServicePubkey: HexKey = - walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + walletKeyPair.pubKey.toHexKey() val event = LnZapPaymentRequestEvent.create( @@ -139,7 +140,7 @@ class LnZapPaymentRequestEventTest { val clientSigner = NostrSignerInternal(clientKeyPair) val walletSigner = NostrSignerInternal(walletKeyPair) val walletServicePubkey: HexKey = - walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + walletKeyPair.pubKey.toHexKey() val request = MakeInvoiceMethod.create(5000L, "test payment") val event = @@ -163,7 +164,7 @@ class LnZapPaymentRequestEventTest { val clientSigner = NostrSignerInternal(clientKeyPair) val walletSigner = NostrSignerInternal(walletKeyPair) val walletServicePubkey: HexKey = - walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + walletKeyPair.pubKey.toHexKey() val request = GetInfoMethod.create() val event = @@ -191,7 +192,7 @@ class LnZapPaymentRequestEventTest { val walletSigner = NostrSignerInternal(walletKeyPair) val otherSigner = NostrSignerInternal(otherKeyPair) val walletServicePubkey: HexKey = - walletKeyPair.pubKey.joinToString("") { "%02x".format(it) } + walletKeyPair.pubKey.toHexKey() val event = LnZapPaymentRequestEvent.create( diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt index c56dd72b1b..b0ba1d6b7d 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/ElectrumXClient.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin +import com.vitorpamplona.quartz.nip01Core.core.toHexKey import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -277,7 +278,7 @@ class ElectrumXClient( */ private fun electrumScriptHash(script: ByteArray): String { val digest = MessageDigest.getInstance("SHA-256").digest(script) - return digest.reversedArray().joinToString("") { "%02x".format(it) } + return digest.reversedArray().toHexKey() } /** From 0380927f7e31b5cbca394dc2df8505cf9840b641 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 11:05:40 -0400 Subject: [PATCH 101/172] fromJsonTo calls throw, they should always be wrapped if we want null results. --- .../vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt index b06d3710ca..163d258416 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt @@ -91,7 +91,7 @@ class NotificationTest { @Test fun testUnknownNotificationTypeReturnsNull() { val json = """{"notification_type":"unknown_type","notification":{}}""" - val notification = OptimizedJsonMapper.fromJsonTo(json) + val notification = runCatching { OptimizedJsonMapper.fromJsonTo(json) }.getOrNull() assertNull(notification) } } From 68f58c3b8d2cadce9dc2304e0a9988edf520f9e6 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 15 Mar 2026 15:06:39 +0000 Subject: [PATCH 102/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-hi-rIN/strings.xml | 41 ++++++++++++++++--- .../src/main/res/values-hu-rHU/strings.xml | 8 ++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 6332634f02..0c808a742b 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -1068,6 +1068,13 @@ संख्या (साट्स) विवरण (विकल्पात्मक) चालान बनाएँ + चालान बनाया जा रहा है… + चालान अनुकृति + अभी कोई लेनदेन नहीं + आवहन चालू… + प्राप्त + भेजा गया + नवीकरण सुरक्षार्थ छलनियाँ अनुचरित आयात करें नया पत्र प्रकाशन @@ -1104,8 +1111,8 @@ पुनःप्रसारक सूची चयनकर्ता मतदान मतदान अक्षम करें - बिटकोयिन चालान - बिटकोयिन चालान निरस्त करें + द्व्यंकरूप्य चालान + द्व्यंकरूप्य चालान निरस्त करें वस्तु बिक्री निरस्त करें ज्सापोपार्जन योजना ज्सापोपार्जन योजना निरस्त करें @@ -1210,7 +1217,7 @@ अनुकृति : ओ॰टी॰एस : %1$s समयांकन प्रमाण - प्रमाण उपलब्ध है कि इस पत्र पर हस्ताक्षर किया गया %1$s के कुछ पहले। प्रमाण अंकित किया गया बिटकोयिन खण्डश्रृंखला में उस समय उस दिन पर। + प्रमाण उपलब्ध है कि इस पत्र पर हस्ताक्षर किया गया %1$s के कुछ पहले। प्रमाण अंकित किया गया द्व्यंकरूप्य खण्डश्रृंखला में उस समय उस दिन पर। पत्र का सम्पादन करें पत्र शोधन के लिए प्रस्ताव परिवर्तनों का साराम्श @@ -1445,8 +1452,8 @@ अभिलेख शीर्षक परिचय चित्रालय अभिलेख सेवासंगणक - अंकीय तथ्याभिलेख - अंकीय अभिलेख शीर्षक + द्व्यंकीय अभिलेख + द्व्यंकीय अभिलेख शीर्षक चिकित्सा अभिलेख अनुचरण पोटलियाँ पुनःप्रकाशन (१६) @@ -1527,4 +1534,28 @@ अनुचरित सूची आयात प्रयोक्ता चुनें अनुचरण करने के लिए प्रयोक्ता परिचय जिससे आयात करना है + खोज, एनपुब॰१…, alice@example.com + एनपुब॰ एन॰परिचय निप॰०५ षोडशांक तथा नामरूप्य का आलम्बन करता है (.bit, d/, id/) + अनुचरण सूची देखें + पारितोषिक + %1$d लेखाएँ प्राप्त + %1$d चयनित + नामरूप्य द्वारा सुलझा गया + अब %1$d लेखाएँ अनुचरित + आपकी सूचनावली उपलब्ध है। + छोडें + अन्य खोजें + %1$d लेखाओं का अनुचरण करें + अधिक आयात + चलते रहें + अभी के लिए छोडें + %1$s का सुलझन चालू… + अनुचरण सूची प्राप्त की जा रही है… + कोई अनुचरित नहीं + %1$d लेखाओं का अनुचरण… + "किसी मित्र अथवा समूह नेता का परिचय प्रविष्ट करें। उनके एनपुब॰ अथवा निप॰०५ पता अथवा नामरूप्य नाम जैसे कि alice@example.com अथवा id/alice का उपयोग आप कर सकते हैं खण्डश्रृंखला सत्यापित विभेदकों के लिए।" + सभी चुनें + %1$d%% समय निरन्तर उपलब्ध + नामरूप्य स्थापना विकल्प + द्व्यंकरूप्य समन्वेषक (ओटीएस॰) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index cdaa9bb882..f4b6a74283 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -1052,6 +1052,14 @@ Globális Rövidek Sakk + Pénztárca + Küldés + Fogadás + Tranzakciók + Pénztárca beállítása + Egy BOLT-11 számla beillesztése + Fizetés + Sikeres fizetés Biztonsági szűrők Követettek importálása Új bejegyzés From 2b157cc3833e425d1633c868c30bf513799379cd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 11:18:39 -0400 Subject: [PATCH 103/172] Fixes throw vs null test cases and nsec hallucinations --- .../nip47WalletConnect/NotificationTest.kt | 51 ++++++++++--------- .../nip47WalletConnect/NwcInfoEventTest.kt | 2 +- .../quartz/nip47WalletConnect/RequestTest.kt | 6 ++- .../core/OptimizedJsonMapper.jvmAndroid.kt | 5 ++ 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt index 163d258416..39fd0f56b8 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NotificationTest.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip47WalletConnect import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -35,16 +36,16 @@ class NotificationTest { val notification = OptimizedJsonMapper.fromJsonTo(json) assertIs(notification) assertNotNull(notification.notification) - assertEquals("incoming", notification.notification?.type) - assertEquals("lnbc50n1...", notification.notification?.invoice) - assertEquals("coffee", notification.notification?.description) - assertEquals("abc", notification.notification?.preimage) - assertEquals("hash123", notification.notification?.payment_hash) - assertEquals(5000L, notification.notification?.amount) - assertEquals(10L, notification.notification?.fees_paid) - assertEquals(1693876497L, notification.notification?.created_at) - assertEquals(1694876497L, notification.notification?.expires_at) - assertEquals(1694876500L, notification.notification?.settled_at) + assertEquals("incoming", notification.notification.type) + assertEquals("lnbc50n1...", notification.notification.invoice) + assertEquals("coffee", notification.notification.description) + assertEquals("abc", notification.notification.preimage) + assertEquals("hash123", notification.notification.payment_hash) + assertEquals(5000L, notification.notification.amount) + assertEquals(10L, notification.notification.fees_paid) + assertEquals(1693876497L, notification.notification.created_at) + assertEquals(1694876497L, notification.notification.expires_at) + assertEquals(1694876500L, notification.notification.settled_at) } @Test @@ -54,11 +55,11 @@ class NotificationTest { val notification = OptimizedJsonMapper.fromJsonTo(json) assertIs(notification) assertNotNull(notification.notification) - assertEquals("outgoing", notification.notification?.type) - assertEquals("lnbc100n1...", notification.notification?.invoice) - assertEquals("def456", notification.notification?.preimage) - assertEquals(10000L, notification.notification?.amount) - assertEquals(50L, notification.notification?.fees_paid) + assertEquals("outgoing", notification.notification.type) + assertEquals("lnbc100n1...", notification.notification.invoice) + assertEquals("def456", notification.notification.preimage) + assertEquals(10000L, notification.notification.amount) + assertEquals(50L, notification.notification.fees_paid) } @Test @@ -68,13 +69,13 @@ class NotificationTest { val notification = OptimizedJsonMapper.fromJsonTo(json) assertIs(notification) assertNotNull(notification.notification) - assertEquals("incoming", notification.notification?.type) - assertEquals("lnbc200n1...", notification.notification?.invoice) - assertEquals("hash789", notification.notification?.payment_hash) - assertEquals(20000L, notification.notification?.amount) - assertEquals(800000L, notification.notification?.settle_deadline) - assertEquals(1693876497L, notification.notification?.created_at) - assertEquals(1694876497L, notification.notification?.expires_at) + assertEquals("incoming", notification.notification.type) + assertEquals("lnbc200n1...", notification.notification.invoice) + assertEquals("hash789", notification.notification.payment_hash) + assertEquals(20000L, notification.notification.amount) + assertEquals(800000L, notification.notification.settle_deadline) + assertEquals(1693876497L, notification.notification.created_at) + assertEquals(1694876497L, notification.notification.expires_at) } @Test @@ -89,9 +90,11 @@ class NotificationTest { } @Test + @Throws(IllegalArgumentException::class) fun testUnknownNotificationTypeReturnsNull() { val json = """{"notification_type":"unknown_type","notification":{}}""" - val notification = runCatching { OptimizedJsonMapper.fromJsonTo(json) }.getOrNull() - assertNull(notification) + assertFailsWith { + OptimizedJsonMapper.fromJsonTo(json) + } } } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt index 3a8b15ca8c..82d55cc645 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcInfoEventTest.kt @@ -28,7 +28,7 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class NwcInfoEventTest { - private val signer = DeterministicSigner("nsec1w4uucmeyyng0kegm7486r23sv4majkmvqsj6eypprq0xttxss55s5mgg9t".nsecToKeyPair()) + private val signer = DeterministicSigner("nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToKeyPair()) @Test fun testBuildInfoEvent() { diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt index 21816b6473..5b43e7bf9f 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/RequestTest.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip47WalletConnect import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -396,7 +397,8 @@ class RequestTest { @Test fun testUnknownMethodReturnsNull() { val json = """{"method":"unknown_method","params":{}}""" - val request = OptimizedJsonMapper.fromJsonTo(json) - assertNull(request) + assertFailsWith { + OptimizedJsonMapper.fromJsonTo(json) + } } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.jvmAndroid.kt index 65af774218..5a501c070b 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.jvmAndroid.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.nip01Core.core +import com.fasterxml.jackson.databind.RuntimeJsonMappingException import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command @@ -78,6 +79,10 @@ actual object OptimizedJsonMapper { JacksonMapper.fromJsonTo(json) } catch (e: com.fasterxml.jackson.core.JsonParseException) { throw IllegalArgumentException(e.message, e) + } catch (e: com.fasterxml.jackson.core.JsonProcessingException) { + throw IllegalArgumentException(e.message, e) + } catch (e: RuntimeJsonMappingException) { + throw IllegalArgumentException(e.message, e) } actual fun toJson(value: OptimizedSerializable): String = JacksonMapper.toJson(value) From 3bd6ef5f6c5f943cdc7d8dc6d4d33ca927ff897c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 11:44:20 -0400 Subject: [PATCH 104/172] changes from damus to nos.lol for more stable relay behavior in tests --- .../quartz/nip01Core/relay/NostrClientManualSubTest.kt | 2 +- .../quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt | 2 +- .../quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt | 2 +- .../relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt index bff422a78b..338a832e8b 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt @@ -81,7 +81,7 @@ class NostrClientManualSubTest : BaseNostrClientTest() { client.openReqSubscription(mySubId, filters, listener) - withTimeoutOrNull(30000) { + withTimeoutOrNull(10000) { while (events.size < 101) { val event = resultChannel.receive() events.add(event) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt index 5f289a4c61..e942fe1345 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSendAndWaitTest.kt @@ -48,7 +48,7 @@ class NostrClientSendAndWaitTest : BaseNostrClientTest() { val resultDamus = client.sendAndWaitForResponse( event = event, - relayList = setOf("wss://relay.damus.io".normalizeRelayUrl()), + relayList = setOf("wss://nostr.bitcoiner.social".normalizeRelayUrl()), ) val resultNos = diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt index f7faca43c4..e28b887f2b 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt @@ -54,7 +54,7 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() { val flow = client.reqAsFlow( - relay = "wss://relay.damus.io", + relay = "wss://nos.lol", filter = Filter( kinds = listOf(MetadataEvent.KIND), diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt index ddcb9fde94..913bf5eff9 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt @@ -54,7 +54,7 @@ class NostrClientSubscriptionUntilEoseAsFlowTest : BaseNostrClientTest() { val flow = client.reqUntilEoseAsFlow( - relay = "wss://relay.damus.io", + relay = "wss://nos.lol", filter = Filter( kinds = listOf(MetadataEvent.KIND), From 17c3611f503951c19612331b4d3eee63c65d236c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 15:46:56 +0000 Subject: [PATCH 105/172] feat: redesign RelayEventCountRow with pill chip style Replace the plain icon+text count display with rounded pill chips that have a subtle green border and tinted background. Each count entry gets its own pill with an icon and bold text, making the event counts more visually distinct and scannable. https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA --- .../relays/common/RelayEventCountRow.kt | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt index c1858eb9d7..b62432f4b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt @@ -20,10 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Storage import androidx.compose.material3.Icon @@ -32,15 +36,17 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.countToHumanReadable import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.Font12SP -import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding -import com.vitorpamplona.amethyst.ui.theme.Size15Modifier +import com.vitorpamplona.amethyst.ui.theme.Font10SP +import com.vitorpamplona.amethyst.ui.theme.Size10Modifier import com.vitorpamplona.amethyst.ui.theme.allGoodColor -import com.vitorpamplona.amethyst.ui.theme.placeholderText + +private val PillShape = RoundedCornerShape(12.dp) @Composable fun RelayEventCountRow( @@ -49,37 +55,51 @@ fun RelayEventCountRow( ) { if (countResult == null || countResult.counts.isEmpty()) return + val pillColor = MaterialTheme.colorScheme.allGoodColor + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, modifier = modifier, ) { - Icon( - imageVector = Icons.Default.Storage, - contentDescription = stringRes(R.string.relay_event_count), - modifier = Size15Modifier, - tint = MaterialTheme.colorScheme.allGoodColor, - ) - countResult.counts.forEachIndexed { index, entry -> if (index > 0) { - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.width(6.dp)) } - val text = + val countText = if (entry.approximate) { "~${countToHumanReadable(entry.count, entry.label)}" } else { countToHumanReadable(entry.count, entry.label) } - Text( - text = text, - maxLines = 1, - fontSize = Font12SP, - modifier = HalfStartPadding, - color = MaterialTheme.colorScheme.placeholderText, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier + .clip(PillShape) + .border(width = 1.dp, color = pillColor.copy(alpha = 0.4f), shape = PillShape) + .background(pillColor.copy(alpha = 0.1f)) + .padding(horizontal = 6.dp, vertical = 2.dp), + ) { + Icon( + imageVector = Icons.Default.Storage, + contentDescription = stringRes(R.string.relay_event_count), + modifier = Size10Modifier, + tint = pillColor, + ) + + Spacer(modifier = Modifier.width(3.dp)) + + Text( + text = countText, + maxLines = 1, + fontSize = Font10SP, + fontWeight = FontWeight.Medium, + color = pillColor, + ) + } } } } From d8de0a9b84c740a00455efd58b8a35c72745df14 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 15 Mar 2026 15:47:20 +0000 Subject: [PATCH 106/172] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-hu-rHU/strings.xml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index f4b6a74283..8615b4bfb1 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -418,7 +418,7 @@ Áthelyezés a nyilvános könyvjelzőkbe Áthelyezés a privát könyvjelzőkbe Wallet Connect szolgáltatás - Hitelesíti, hogy a Nostr Secret az alkalmazásból való kilépés nélkül fizessen a Zap-et. Tartsa biztonságban a titkot, és lehetőség szerint használjon privát átjátszót + Lehetővé teszi az Amethyst számára, hogy az alkalmazásból kilépés nélkül fizessen. Tartsa biztonságos helyen a titkot! Wallet Connect nyilvános kulcs Wallet Connect átjátszó Wallet Connect titok @@ -1053,13 +1053,28 @@ Rövidek Sakk Pénztárca + Egyenleg Küldés Fogadás Tranzakciók + Nincs pénztárca összekapcsolva + A pénztárca használatához állítson be egy Nostr Wallet Connect (NWC) kapcsolatot a zap-beállításokban. Pénztárca beállítása + satoshik Egy BOLT-11 számla beillesztése Fizetés Sikeres fizetés + Fizetés küldése… + Összeg (satoshiban) + Leírás (nem kötelező) + Számla létrehozása + Számla létrehozása… + Számla másolása + Még nincsenek tranzakciók + Betöltés… + Fogadott + Elküldött + Frissítés Biztonsági szűrők Követettek importálása Új bejegyzés From 22945b7faa5ae8b418461eef2c3a79d029fcbaff Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 16:14:24 +0000 Subject: [PATCH 107/172] feat: add queryCountSuspend utility to Quartz and simplify ViewModels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add INostrClient.queryCountSuspend() extension functions in Quartz that wrap NIP-45 COUNT queries as suspend functions, managing subscription lifecycle internally. Two overloads: single-relay and multi-relay. Simplify BasicRelaySetupInfoModel and Nip65RelayListViewModel to use the new utility — removes IRelayClientListener implementation, subId tracking maps, onIncomingMessage handlers, and cleanup logic from both ViewModels. https://claude.ai/code/session_016158D5mq5BygS1uBbLNbsA --- .../relays/common/BasicRelaySetupInfoModel.kt | 105 +++----------- .../relays/nip65/Nip65RelayListViewModel.kt | 118 ++++++--------- .../client/accessories/NostrClientCountExt.kt | 136 ++++++++++++++++++ 3 files changed, 200 insertions(+), 159 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientCountExt.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index 466cc16820..e198edcc7b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -26,11 +26,7 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener -import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient -import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId -import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage -import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.queryCountSuspend import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -38,7 +34,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -abstract class BasicRelaySetupInfoModel : ViewModel(), IRelayClientListener { +abstract class BasicRelaySetupInfoModel : ViewModel() { lateinit var accountViewModel: AccountViewModel lateinit var account: Account @@ -48,9 +44,6 @@ abstract class BasicRelaySetupInfoModel : ViewModel(), IRelayClientListener { private val _countResults = MutableStateFlow>(emptyMap()) val countResults = _countResults.asStateFlow() - private val subIdToRelay = mutableMapOf>() - private val relayQueryInfos = mutableMapOf>() - var hasModified = false fun init(accountViewModel: AccountViewModel) { @@ -94,82 +87,39 @@ abstract class BasicRelaySetupInfoModel : ViewModel(), IRelayClientListener { } private fun loadCounts() { - val client = Amethyst.instance.client - cleanupCounts() + _countResults.value = emptyMap() + val client = Amethyst.instance.client val relayList = _relays.value if (relayList.isEmpty()) return - val hasFilters = relayList.any { countFilters(it.relay).isNotEmpty() } - if (!hasFilters) return - - client.subscribe(this) - relayList.forEach { item -> val filters = countFilters(item.relay) if (filters.isEmpty()) return@forEach - val queryInfos = mutableListOf() - - filters.forEachIndexed { index, countFilter -> - val subId = newSubId() - subIdToRelay[subId] = Pair(item.relay, index) - queryInfos.add(CountQueryInfo(subId, countFilter.label, index)) - - client.queryCount( - subId = subId, - filters = mapOf(item.relay to listOf(countFilter.filter)), - ) - } - - relayQueryInfos[item.relay] = queryInfos - } - } - - override fun onIncomingMessage( - relay: IRelayClient, - msgStr: String, - msg: Message, - ) { - if (msg is CountMessage) { - val (relayUrl, filterIndex) = subIdToRelay[msg.queryId] ?: return - val queryInfos = relayQueryInfos[relayUrl] ?: return - val queryInfo = queryInfos.find { it.filterIndex == filterIndex } ?: return - - _countResults.update { currentMap -> - val currentResult = currentMap[relayUrl] ?: RelayCountResult() - val updatedEntries = currentResult.counts.toMutableList() - - val existingIndex = updatedEntries.indexOfFirst { it.label == queryInfo.label } - val newEntry = - RelayCountResult.CountEntry( - label = queryInfo.label, - count = msg.result.count, - approximate = msg.result.approximate, - ) - - if (existingIndex >= 0) { - updatedEntries[existingIndex] = newEntry - } else { - updatedEntries.add(newEntry) + filters.forEach { countFilter -> + viewModelScope.launch(Dispatchers.IO) { + val result = client.queryCountSuspend(item.relay, countFilter.filter) + if (result != null) { + _countResults.update { currentMap -> + val current = currentMap[item.relay] ?: RelayCountResult() + val entries = current.counts.toMutableList() + val newEntry = + RelayCountResult.CountEntry( + label = countFilter.label, + count = result.count, + approximate = result.approximate, + ) + val existing = entries.indexOfFirst { it.label == countFilter.label } + if (existing >= 0) entries[existing] = newEntry else entries.add(newEntry) + currentMap + (item.relay to RelayCountResult(entries)) + } + } } - - currentMap + (relayUrl to RelayCountResult(updatedEntries)) } } } - private fun cleanupCounts() { - val client = Amethyst.instance.client - subIdToRelay.keys.forEach { subId -> - client.close(subId) - } - subIdToRelay.clear() - relayQueryInfos.clear() - _countResults.value = emptyMap() - client.unsubscribe(this) - } - open fun relayListBuilder(): List { val relayList = getRelayList() ?: emptyList() @@ -213,15 +163,4 @@ abstract class BasicRelaySetupInfoModel : ViewModel(), IRelayClientListener { ) { _relays.update { it.replace(relay, relay.copy(paidRelay = paid)) } } - - override fun onCleared() { - cleanupCounts() - super.onCleared() - } - - private data class CountQueryInfo( - val subId: String, - val label: String, - val filterIndex: Int, - ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index fa1427b892..0894a0f047 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -30,11 +30,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayCountResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder -import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener -import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient -import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId -import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage -import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.queryCountSuspend import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip65RelayList.tags.AdvertisedRelayInfo @@ -46,7 +42,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @Stable -class Nip65RelayListViewModel : ViewModel(), IRelayClientListener { +class Nip65RelayListViewModel : ViewModel() { private lateinit var accountViewModel: AccountViewModel private lateinit var account: Account @@ -62,8 +58,6 @@ class Nip65RelayListViewModel : ViewModel(), IRelayClientListener { private val _notifCountResults = MutableStateFlow>(emptyMap()) val notifCountResults = _notifCountResults.asStateFlow() - private val subIdToRelay = mutableMapOf>() - var hasModified = false fun init(accountViewModel: AccountViewModel) { @@ -129,71 +123,48 @@ class Nip65RelayListViewModel : ViewModel(), IRelayClientListener { } private fun loadCounts() { - val client = Amethyst.instance.client - cleanupCounts() - - val homeList = _homeRelays.value - val notifList = _notificationRelays.value - - if (homeList.isEmpty() && notifList.isEmpty()) return - - client.subscribe(this) - - homeList.forEach { item -> - val subId = newSubId() - subIdToRelay[subId] = Pair(item.relay, true) - client.queryCount( - subId = subId, - filters = mapOf(item.relay to listOf(Filter(authors = listOf(account.pubKey)))), - ) - } - - notifList.forEach { item -> - val subId = newSubId() - subIdToRelay[subId] = Pair(item.relay, false) - client.queryCount( - subId = subId, - filters = mapOf(item.relay to listOf(Filter(tags = mapOf("p" to listOf(account.pubKey))))), - ) - } - } - - override fun onIncomingMessage( - relay: IRelayClient, - msgStr: String, - msg: Message, - ) { - if (msg is CountMessage) { - val (relayUrl, isHome) = subIdToRelay[msg.queryId] ?: return - - val newResult = - RelayCountResult( - listOf( - RelayCountResult.CountEntry( - label = "events", - count = msg.result.count, - approximate = msg.result.approximate, - ), - ), - ) - - if (isHome) { - _homeCountResults.update { it + (relayUrl to newResult) } - } else { - _notifCountResults.update { it + (relayUrl to newResult) } - } - } - } - - private fun cleanupCounts() { - val client = Amethyst.instance.client - subIdToRelay.keys.forEach { subId -> - client.close(subId) - } - subIdToRelay.clear() _homeCountResults.value = emptyMap() _notifCountResults.value = emptyMap() - client.unsubscribe(this) + + val client = Amethyst.instance.client + + _homeRelays.value.forEach { item -> + viewModelScope.launch(Dispatchers.IO) { + val result = client.queryCountSuspend(item.relay, Filter(authors = listOf(account.pubKey))) + if (result != null) { + val countResult = + RelayCountResult( + listOf( + RelayCountResult.CountEntry( + label = "events", + count = result.count, + approximate = result.approximate, + ), + ), + ) + _homeCountResults.update { it + (item.relay to countResult) } + } + } + } + + _notificationRelays.value.forEach { item -> + viewModelScope.launch(Dispatchers.IO) { + val result = client.queryCountSuspend(item.relay, Filter(tags = mapOf("p" to listOf(account.pubKey)))) + if (result != null) { + val countResult = + RelayCountResult( + listOf( + RelayCountResult.CountEntry( + label = "events", + count = result.count, + approximate = result.approximate, + ), + ), + ) + _notifCountResults.update { it + (item.relay to countResult) } + } + } + } } fun clear() { @@ -266,9 +237,4 @@ class Nip65RelayListViewModel : ViewModel(), IRelayClientListener { ) { _notificationRelays.update { it.replace(relay, relay.copy(paidRelay = paid)) } } - - override fun onCleared() { - cleanupCounts() - super.onCleared() - } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientCountExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientCountExt.kt new file mode 100644 index 0000000000..b1ea3d05d7 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/NostrClientCountExt.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay.client.accessories + +import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountResult +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED +import kotlinx.coroutines.withTimeoutOrNull + +/** + * Sends a NIP-45 COUNT query to a single relay and suspends until + * the result arrives or the timeout expires. + * + * @param relay Target relay to query. + * @param filter The filter to count against. + * @param timeoutMs How long to wait for a response (default 15 s). + * @return The [CountResult], or `null` on timeout. + */ +suspend fun INostrClient.queryCountSuspend( + relay: NormalizedRelayUrl, + filter: Filter, + timeoutMs: Long = 15_000, +): CountResult? { + val subId = newSubId() + val resultChannel = Channel(UNLIMITED) + + val listener = + object : IRelayClientListener { + override fun onIncomingMessage( + relay: IRelayClient, + msgStr: String, + msg: Message, + ) { + if (msg is CountMessage && msg.queryId == subId) { + resultChannel.trySend(msg.result) + } + } + } + + subscribe(listener) + + queryCount(subId = subId, filters = mapOf(relay to listOf(filter))) + + val result = + withTimeoutOrNull(timeoutMs) { + resultChannel.receive() + } + + close(subId) + unsubscribe(listener) + resultChannel.close() + + return result +} + +/** + * Sends NIP-45 COUNT queries to multiple relays in parallel + * (one filter per relay) and suspends until all results arrive + * or the timeout expires. + * + * @param filters Map of relay -> filter to count. + * @param timeoutMs How long to wait for all responses (default 15 s). + * @return Map of relay -> [CountResult] for every relay that responded in time. + */ +suspend fun INostrClient.queryCountSuspend( + filters: Map>, + timeoutMs: Long = 15_000, +): Map { + if (filters.isEmpty()) return emptyMap() + + val subIdToRelay = mutableMapOf() + val resultChannel = Channel>(UNLIMITED) + + val listener = + object : IRelayClientListener { + override fun onIncomingMessage( + relay: IRelayClient, + msgStr: String, + msg: Message, + ) { + if (msg is CountMessage) { + val relayUrl = subIdToRelay[msg.queryId] ?: return + resultChannel.trySend(relayUrl to msg.result) + } + } + } + + subscribe(listener) + + filters.forEach { (relay, filterList) -> + val subId = newSubId() + subIdToRelay[subId] = relay + queryCount(subId = subId, filters = mapOf(relay to filterList)) + } + + val results = mutableMapOf() + + withTimeoutOrNull(timeoutMs) { + while (results.size < filters.size) { + val (relay, result) = resultChannel.receive() + results[relay] = result + } + } + + subIdToRelay.keys.forEach { close(it) } + unsubscribe(listener) + resultChannel.close() + + return results +} From ffac6822380c663ff798ebbeac3c45ae47308a33 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 16:37:22 +0000 Subject: [PATCH 108/172] feat: Add kotlinx.serialization KSerializers mirroring all Jackson custom serializers Add 15 KSerializer implementations in commonMain that replicate the exact behavior of all 21 Jackson serializer/deserializer registrations: - EventKSerializer, TagArrayKSerializer, FilterKSerializer - EventTemplateKSerializer, RumorKSerializer, CountResultKSerializer - MessageKSerializer (8 message types), CommandKSerializer (5 command types) - BunkerRequest/Response/MessageKSerializer (NIP-46) - Nip47Request/Response/NotificationKSerializer (NIP-47) - KotlinSerializationMapper as the main entry point Uses JsonElement-based approach for performance. All serializers produce identical JSON output to Jackson. Includes comprehensive test suite (35+ tests) verifying exact output parity and cross-deserialization between Jackson and kotlinx.serialization. https://claude.ai/code/session_01QBc4Pb7a6m4TfLZSJKVdjw --- .../BunkerMessageKSerializer.kt | 97 +++ .../BunkerRequestKSerializer.kt | 100 +++ .../BunkerResponseKSerializer.kt | 110 +++ .../kotlinSerialization/CommandKSerializer.kt | 116 +++ .../CountResultKSerializer.kt | 72 ++ .../kotlinSerialization/EventKSerializer.kt | 111 +++ .../EventTemplateKSerializer.kt | 87 ++ .../kotlinSerialization/FilterKSerializer.kt | 141 ++++ .../KotlinSerializationMapper.kt | 102 +++ .../kotlinSerialization/MessageKSerializer.kt | 136 ++++ .../Nip47NotificationKSerializer.kt | 94 +++ .../Nip47RequestKSerializer.kt | 241 ++++++ .../Nip47ResponseKSerializer.kt | 263 +++++++ .../kotlinSerialization/RumorKSerializer.kt | 93 +++ .../TagArrayKSerializer.kt | 101 +++ .../KotlinSerializationMapperTest.kt | 740 ++++++++++++++++++ 16 files changed, 2604 insertions(+) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerMessageKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerRequestKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerResponseKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CountResultKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventTemplateKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47NotificationKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47RequestKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47ResponseKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/RumorKSerializer.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt create mode 100644 quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerMessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerMessageKSerializer.kt new file mode 100644 index 0000000000..157be1b957 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerMessageKSerializer.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +object BunkerMessageKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("BunkerMessage") + + override fun serialize( + encoder: Encoder, + value: BunkerMessage, + ) { + val jsonEncoder = encoder as JsonEncoder + when (value) { + is BunkerRequest -> BunkerRequestKSerializer.serialize(jsonEncoder, value) + is BunkerResponse -> BunkerResponseKSerializer.serialize(jsonEncoder, value) + else -> throw IllegalArgumentException("Unknown BunkerMessage type") + } + } + + override fun deserialize(decoder: Decoder): BunkerMessage { + val jsonDecoder = decoder as JsonDecoder + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + val isRequest = jsonObject.containsKey("method") + + return if (isRequest) { + val id = jsonObject["id"]!!.jsonPrimitive.content + val method = jsonObject["method"]!!.jsonPrimitive.content + val params = + jsonObject["params"]?.jsonArray?.map { it.jsonPrimitive.content }?.toTypedArray() + ?: emptyArray() + dispatchBunkerRequest(id, method, params) + } else { + BunkerResponseKSerializer.deserializeFromElement(jsonObject) + } + } + + private fun dispatchBunkerRequest( + id: String, + method: String, + params: Array, + ): BunkerRequest { + return when (method) { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.parse(id, params) + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.parse(id, params) + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.parse(id, params) + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.parse(id, params) + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.parse(id, params) + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.parse(id, params) + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.parse(id, params) + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.parse(id, params) + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.METHOD_NAME -> + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.parse(id, params) + else -> BunkerRequest(id, method, params) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerRequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerRequestKSerializer.kt new file mode 100644 index 0000000000..3d748bfd0c --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerRequestKSerializer.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put + +object BunkerRequestKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("BunkerRequest") { + element("id") + element("method") + element>("params") + } + + override fun serialize( + encoder: Encoder, + value: BunkerRequest, + ) { + val jsonEncoder = encoder as JsonEncoder + val element = + buildJsonObject { + put("id", value.id) + put("method", value.method) + put( + "params", + buildJsonArray { + for (p in value.params) { + add(JsonPrimitive(p)) + } + }, + ) + } + jsonEncoder.encodeJsonElement(element) + } + + override fun deserialize(decoder: Decoder): BunkerRequest { + val jsonDecoder = decoder as JsonDecoder + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + val id = jsonObject["id"]!!.jsonPrimitive.content + val method = jsonObject["method"]!!.jsonPrimitive.content + val params = + jsonObject["params"]?.jsonArray?.map { it.jsonPrimitive.content }?.toTypedArray() + ?: emptyArray() + + return when (method) { + BunkerRequestConnect.METHOD_NAME -> BunkerRequestConnect.parse(id, params) + BunkerRequestGetPublicKey.METHOD_NAME -> BunkerRequestGetPublicKey.parse(id, params) + BunkerRequestGetRelays.METHOD_NAME -> BunkerRequestGetRelays.parse(id, params) + BunkerRequestNip04Decrypt.METHOD_NAME -> BunkerRequestNip04Decrypt.parse(id, params) + BunkerRequestNip04Encrypt.METHOD_NAME -> BunkerRequestNip04Encrypt.parse(id, params) + BunkerRequestNip44Decrypt.METHOD_NAME -> BunkerRequestNip44Decrypt.parse(id, params) + BunkerRequestNip44Encrypt.METHOD_NAME -> BunkerRequestNip44Encrypt.parse(id, params) + BunkerRequestPing.METHOD_NAME -> BunkerRequestPing.parse(id, params) + BunkerRequestSign.METHOD_NAME -> BunkerRequestSign.parse(id, params) + else -> BunkerRequest(id, method, params) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerResponseKSerializer.kt new file mode 100644 index 0000000000..bf4cbbaf6e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerResponseKSerializer.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseAck +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseGetRelays +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey +import com.vitorpamplona.quartz.utils.Hex +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put + +object BunkerResponseKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("BunkerResponse") { + element("id") + element("result") + element("error") + } + + override fun serialize( + encoder: Encoder, + value: BunkerResponse, + ) { + val jsonEncoder = encoder as JsonEncoder + val element = + buildJsonObject { + put("id", value.id) + value.result?.let { put("result", it) } + value.error?.let { put("error", it) } + } + jsonEncoder.encodeJsonElement(element) + } + + override fun deserialize(decoder: Decoder): BunkerResponse { + val jsonDecoder = decoder as JsonDecoder + return deserializeFromElement(jsonDecoder.decodeJsonElement().jsonObject) + } + + fun deserializeFromElement(jsonObject: JsonObject): BunkerResponse { + val id = jsonObject["id"]!!.jsonPrimitive.content + val result = jsonObject["result"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content } + val error = jsonObject["error"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content } + + if (error != null) { + return BunkerResponseError.parse(id, result, error) + } + + if (result != null) { + when (result) { + BunkerResponseAck.RESULT -> return BunkerResponseAck.parse(id, result, error) + BunkerResponsePong.RESULT -> return BunkerResponsePong.parse(id, result, error) + else -> { + if (result.length == 64 && Hex.isHex(result)) { + return BunkerResponsePublicKey.parse(id, result) + } + + if (result.isNotEmpty() && result[0] == '{') { + try { + return BunkerResponseEvent.parse(id, result) + } catch (_: Exception) { + } + + try { + return BunkerResponseGetRelays.parse(id, result) + } catch (_: Exception) { + } + } + + return BunkerResponse(id, result, error) + } + } + } + + return BunkerResponse(id, result, error) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt new file mode 100644 index 0000000000..790a710ddf --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.AuthCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CountCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +object CommandKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Command") + + override fun serialize( + encoder: Encoder, + value: Command, + ) { + val jsonEncoder = encoder as JsonEncoder + val element = + buildJsonArray { + add(JsonPrimitive(value.label())) + when (value) { + is ReqCmd -> { + add(JsonPrimitive(value.subId)) + for (filter in value.filters) { + add(FilterKSerializer.serializeToElement(filter)) + } + } + is EventCmd -> { + add(EventKSerializer.serializeToElement(value.event)) + } + is CloseCmd -> { + add(JsonPrimitive(value.subId)) + } + is AuthCmd -> { + add(EventKSerializer.serializeToElement(value.event)) + } + is CountCmd -> { + add(JsonPrimitive(value.queryId)) + for (filter in value.filters) { + add(FilterKSerializer.serializeToElement(filter)) + } + } + } + } + jsonEncoder.encodeJsonElement(element) + } + + override fun deserialize(decoder: Decoder): Command { + val jsonDecoder = decoder as JsonDecoder + val array = jsonDecoder.decodeJsonElement().jsonArray + val type = array[0].jsonPrimitive.content + + return when (type) { + ReqCmd.LABEL -> { + val subId = array[1].jsonPrimitive.content + val filters = + (2 until array.size).map { i -> + FilterKSerializer.deserializeFromElement(array[i].jsonObject) + } + ReqCmd(subId, filters) + } + CountCmd.LABEL -> { + val queryId = array[1].jsonPrimitive.content + val filters = + (2 until array.size).map { i -> + FilterKSerializer.deserializeFromElement(array[i].jsonObject) + } + CountCmd(queryId, filters) + } + EventCmd.LABEL -> { + EventCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject)) + } + CloseCmd.LABEL -> { + CloseCmd(array[1].jsonPrimitive.content) + } + AuthCmd.LABEL -> { + AuthCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject) as RelayAuthEvent) + } + else -> throw IllegalArgumentException("Message $type is not supported") + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CountResultKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CountResultKSerializer.kt new file mode 100644 index 0000000000..ed0ff95aea --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CountResultKSerializer.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountResult +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.put + +object CountResultKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("CountResult") { + element("count") + element("pubkey") + } + + override fun serialize( + encoder: Encoder, + value: CountResult, + ) { + val jsonEncoder = encoder as JsonEncoder + jsonEncoder.encodeJsonElement(serializeToElement(value)) + } + + fun serializeToElement(value: CountResult): JsonObject = + buildJsonObject { + put("count", value.count) + // Matches Jackson's CountResultSerializer which writes "pubkey" for approximate + put("pubkey", value.approximate) + } + + override fun deserialize(decoder: Decoder): CountResult { + val jsonDecoder = decoder as JsonDecoder + return deserializeFromElement(jsonDecoder.decodeJsonElement().jsonObject) + } + + fun deserializeFromElement(jsonObject: JsonObject): CountResult = + CountResult( + count = jsonObject["count"]!!.jsonPrimitive.int, + approximate = jsonObject["approximate"]?.jsonPrimitive?.boolean ?: false, + ) +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt new file mode 100644 index 0000000000..57d2d56cab --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.Kind +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.utils.EventFactory +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.put + +object EventKSerializer : KSerializer { + private val emptyTagArray = emptyArray>() + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Event") { + element("id") + element("pubkey") + element("created_at") + element("kind") + element("tags", TagArrayKSerializer.descriptor) + element("content") + element("sig") + } + + override fun serialize( + encoder: Encoder, + value: Event, + ) { + val jsonEncoder = encoder as JsonEncoder + jsonEncoder.encodeJsonElement(serializeToElement(value)) + } + + fun serializeToElement(event: Event): JsonObject = + buildJsonObject { + put("id", event.id) + put("pubkey", event.pubKey) + put("created_at", event.createdAt) + put("kind", event.kind) + put("tags", TagArrayKSerializer.serializeToElement(event.tags)) + put("content", event.content) + put("sig", event.sig) + } + + override fun deserialize(decoder: Decoder): Event { + val jsonDecoder = decoder as JsonDecoder + return deserializeFromElement(jsonDecoder.decodeJsonElement().jsonObject) + } + + fun deserializeFromElement(jsonObject: JsonObject): Event { + var id: HexKey = "" + var pubKey: HexKey = "" + var createdAt: Long = 0 + var kind: Kind = 0 + var tags: TagArray = emptyTagArray + var content: String = "" + var sig: HexKey = "" + + for ((key, value) in jsonObject) { + when (key) { + "id" -> id = value.jsonPrimitive.content + "pubkey" -> pubKey = value.jsonPrimitive.content + "created_at" -> createdAt = value.jsonPrimitive.long + "kind" -> kind = value.jsonPrimitive.int + "tags" -> tags = TagArrayKSerializer.deserializeFromElement(value) + "content" -> content = value.jsonPrimitive.content + "sig" -> sig = value.jsonPrimitive.content + } + } + + if (pubKey.isEmpty()) { + throw IllegalArgumentException("Event not found") + } + + return EventFactory.create(id, pubKey, createdAt, kind, tags, content, sig) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventTemplateKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventTemplateKSerializer.kt new file mode 100644 index 0000000000..58ca13df3a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventTemplateKSerializer.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.put + +object EventTemplateKSerializer : KSerializer> { + private val emptyTagArray = emptyArray>() + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("EventTemplate") { + element("created_at") + element("kind") + element("tags", TagArrayKSerializer.descriptor) + element("content") + } + + override fun serialize( + encoder: Encoder, + value: EventTemplate, + ) { + val jsonEncoder = encoder as JsonEncoder + val element = + buildJsonObject { + put("created_at", value.createdAt) + put("kind", value.kind) + put("tags", TagArrayKSerializer.serializeToElement(value.tags)) + put("content", value.content) + } + jsonEncoder.encodeJsonElement(element) + } + + override fun deserialize(decoder: Decoder): EventTemplate { + val jsonDecoder = decoder as JsonDecoder + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + + var createdAt = 0L + var kind = 0 + var tags: TagArray = emptyTagArray + var content = "" + + for ((key, value) in jsonObject) { + when (key) { + "created_at" -> createdAt = value.jsonPrimitive.long + "kind" -> kind = value.jsonPrimitive.int + "tags" -> tags = TagArrayKSerializer.deserializeFromElement(value) + "content" -> content = value.jsonPrimitive.content + } + } + + return EventTemplate(createdAt, kind, tags, content) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt new file mode 100644 index 0000000000..318af177ea --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.put + +object FilterKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Filter") + + override fun serialize( + encoder: Encoder, + value: Filter, + ) { + val jsonEncoder = encoder as JsonEncoder + jsonEncoder.encodeJsonElement(serializeToElement(value)) + } + + fun serializeToElement(filter: Filter): JsonObject = + buildJsonObject { + filter.kinds?.let { kinds -> + put( + "kinds", + buildJsonArray { + for (k in kinds) add(JsonPrimitive(k)) + }, + ) + } + filter.ids?.let { ids -> + put( + "ids", + buildJsonArray { + for (id in ids) add(JsonPrimitive(id)) + }, + ) + } + filter.authors?.let { authors -> + put( + "authors", + buildJsonArray { + for (a in authors) add(JsonPrimitive(a)) + }, + ) + } + filter.tags?.let { tags -> + for ((key, values) in tags) { + put( + "#$key", + buildJsonArray { + for (v in values) add(JsonPrimitive(v)) + }, + ) + } + } + filter.tagsAll?.let { tagsAll -> + for ((key, values) in tagsAll) { + put( + "&$key", + buildJsonArray { + for (v in values) add(JsonPrimitive(v)) + }, + ) + } + } + filter.since?.let { put("since", it) } + filter.until?.let { put("until", it) } + filter.limit?.let { put("limit", it) } + filter.search?.let { put("search", it) } + } + + override fun deserialize(decoder: Decoder): Filter { + val jsonDecoder = decoder as JsonDecoder + return deserializeFromElement(jsonDecoder.decodeJsonElement().jsonObject) + } + + fun deserializeFromElement(jsonObject: JsonObject): Filter { + val tags = mutableMapOf>() + val tagsAll = mutableMapOf>() + + for ((key, value) in jsonObject) { + when { + key.startsWith("#") -> { + tags[key.substring(1)] = value.jsonArray.mapNotNull { it.jsonPrimitive.content } + } + key.startsWith("&") -> { + tagsAll[key.substring(1)] = value.jsonArray.mapNotNull { it.jsonPrimitive.content } + } + } + } + + return Filter( + ids = jsonObject["ids"]?.jsonArray?.mapNotNull { it.jsonPrimitive.content }, + authors = jsonObject["authors"]?.jsonArray?.mapNotNull { it.jsonPrimitive.content }, + kinds = jsonObject["kinds"]?.jsonArray?.mapNotNull { it.jsonPrimitive.intOrNull }, + tags = tags.ifEmpty { null }, + tagsAll = tagsAll.ifEmpty { null }, + since = jsonObject["since"]?.jsonPrimitive?.longOrNull, + until = jsonObject["until"]?.jsonPrimitive?.longOrNull, + limit = jsonObject["limit"]?.jsonPrimitive?.intOrNull, + search = jsonObject["search"]?.jsonPrimitive?.content, + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt new file mode 100644 index 0000000000..901bdf099b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse +import com.vitorpamplona.quartz.nip47WalletConnect.Notification +import com.vitorpamplona.quartz.nip47WalletConnect.Request +import com.vitorpamplona.quartz.nip47WalletConnect.Response +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor +import kotlinx.serialization.json.Json + +class KotlinSerializationMapper { + companion object { + val json = + Json { + ignoreUnknownKeys = true + isLenient = true + encodeDefaults = false + } + + fun fromJson(jsonStr: String): Event = json.decodeFromString(EventKSerializer, jsonStr) + + fun fromJsonToMessage(jsonStr: String): Message = json.decodeFromString(MessageKSerializer, jsonStr) + + fun fromJsonToCommand(jsonStr: String): Command = json.decodeFromString(CommandKSerializer, jsonStr) + + fun fromJsonToTagArray(jsonStr: String): TagArray = json.decodeFromString(TagArrayKSerializer, jsonStr) + + fun fromJsonToRumor(jsonStr: String): Rumor = json.decodeFromString(RumorKSerializer, jsonStr) + + fun fromJsonToEventTemplate(jsonStr: String): EventTemplate = json.decodeFromString(EventTemplateKSerializer, jsonStr) + + fun toJson(event: Event): String = json.encodeToString(EventKSerializer, event) + + fun toJson(tags: TagArray): String = json.encodeToString(TagArrayKSerializer, tags) + + fun toJson(value: OptimizedSerializable): String = + when (value) { + is Event -> json.encodeToString(EventKSerializer, value) + is Filter -> json.encodeToString(FilterKSerializer, value) + is Rumor -> json.encodeToString(RumorKSerializer, value) + is EventTemplate<*> -> { + @Suppress("UNCHECKED_CAST") + json.encodeToString(EventTemplateKSerializer, value as EventTemplate) + } + is Message -> json.encodeToString(MessageKSerializer, value) + is Command -> json.encodeToString(CommandKSerializer, value) + is BunkerRequest -> json.encodeToString(BunkerRequestKSerializer, value) + is BunkerResponse -> json.encodeToString(BunkerResponseKSerializer, value) + is BunkerMessage -> json.encodeToString(BunkerMessageKSerializer, value) + else -> throw IllegalArgumentException("Unsupported type: ${value::class}") + } + + inline fun fromJsonTo(jsonStr: String): T { + val result: Any = + when (T::class) { + Event::class -> fromJson(jsonStr) + Filter::class -> json.decodeFromString(FilterKSerializer, jsonStr) + Rumor::class -> fromJsonToRumor(jsonStr) + EventTemplate::class -> fromJsonToEventTemplate(jsonStr) + Message::class -> fromJsonToMessage(jsonStr) + Command::class -> fromJsonToCommand(jsonStr) + BunkerRequest::class -> json.decodeFromString(BunkerRequestKSerializer, jsonStr) + BunkerResponse::class -> json.decodeFromString(BunkerResponseKSerializer, jsonStr) + BunkerMessage::class -> json.decodeFromString(BunkerMessageKSerializer, jsonStr) + Response::class -> json.decodeFromString(Nip47ResponseKSerializer, jsonStr) + Request::class -> json.decodeFromString(Nip47RequestKSerializer, jsonStr) + Notification::class -> json.decodeFromString(Nip47NotificationKSerializer, jsonStr) + else -> throw IllegalArgumentException("Unsupported type: ${T::class}") + } + @Suppress("UNCHECKED_CAST") + return result as T + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt new file mode 100644 index 0000000000..f6e66da708 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NotifyMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive + +object MessageKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Message") + + override fun serialize( + encoder: Encoder, + value: Message, + ) { + val jsonEncoder = encoder as JsonEncoder + val element = + buildJsonArray { + add(JsonPrimitive(value.label())) + when (value) { + is EventMessage -> { + add(JsonPrimitive(value.subId)) + add(EventKSerializer.serializeToElement(value.event)) + } + is NoticeMessage -> { + add(JsonPrimitive(value.message)) + } + is OkMessage -> { + add(JsonPrimitive(value.eventId)) + // Jackson writes success as a string, not boolean + add(JsonPrimitive(value.success.toString())) + if (value.message.isNotBlank()) { + add(JsonPrimitive(value.message)) + } + } + is AuthMessage -> { + add(JsonPrimitive(value.challenge)) + } + is NotifyMessage -> { + add(JsonPrimitive(value.message)) + } + is ClosedMessage -> { + add(JsonPrimitive(value.subId)) + add(JsonPrimitive(value.message)) + } + is CountMessage -> { + add(CountResultKSerializer.serializeToElement(value.result)) + } + } + } + jsonEncoder.encodeJsonElement(element) + } + + override fun deserialize(decoder: Decoder): Message { + val jsonDecoder = decoder as JsonDecoder + val array = jsonDecoder.decodeJsonElement().jsonArray + val type = array[0].jsonPrimitive.content + + return when (type) { + EventMessage.LABEL -> { + val subId = array[1].jsonPrimitive.content + val event = EventKSerializer.deserializeFromElement(array[2].jsonObject) + EventMessage(subId, event) + } + EoseMessage.LABEL -> { + EoseMessage(array[1].jsonPrimitive.content) + } + NoticeMessage.LABEL -> { + NoticeMessage(array[1].jsonPrimitive.content) + } + OkMessage.LABEL -> { + OkMessage( + eventId = array[1].jsonPrimitive.content, + success = array[2].jsonPrimitive.boolean, + message = if (array.size > 3) array[3].jsonPrimitive.content else "", + ) + } + AuthMessage.LABEL -> { + AuthMessage(array[1].jsonPrimitive.content) + } + NotifyMessage.LABEL -> { + NotifyMessage(array[1].jsonPrimitive.content) + } + ClosedMessage.LABEL -> { + ClosedMessage( + subId = array[1].jsonPrimitive.content, + message = if (array.size > 2) array[2].jsonPrimitive.content else "", + ) + } + CountMessage.LABEL -> { + val queryId = array[1].jsonPrimitive.content + val result = CountResultKSerializer.deserializeFromElement(array[2].jsonObject) + CountMessage(queryId, result) + } + else -> throw IllegalArgumentException("Message $type is not supported") + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47NotificationKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47NotificationKSerializer.kt new file mode 100644 index 0000000000..5ac1a412a6 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47NotificationKSerializer.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedData +import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedNotification +import com.vitorpamplona.quartz.nip47WalletConnect.Notification +import com.vitorpamplona.quartz.nip47WalletConnect.NwcNotificationType +import com.vitorpamplona.quartz.nip47WalletConnect.PaymentReceivedNotification +import com.vitorpamplona.quartz.nip47WalletConnect.PaymentSentNotification +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull + +object Nip47NotificationKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Nip47Notification") + + override fun serialize( + encoder: Encoder, + value: Notification, + ) { + throw UnsupportedOperationException("NIP-47 Notification serialization not supported") + } + + override fun deserialize(decoder: Decoder): Notification { + val jsonDecoder = decoder as JsonDecoder + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + val notificationType = + jsonObject["notification_type"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content } + + return when (notificationType) { + NwcNotificationType.PAYMENT_RECEIVED -> { + PaymentReceivedNotification( + notification = + Nip47ResponseKSerializer.parseTransaction( + jsonObject["notification"]?.jsonObject, + ), + ) + } + NwcNotificationType.PAYMENT_SENT -> { + PaymentSentNotification( + notification = + Nip47ResponseKSerializer.parseTransaction( + jsonObject["notification"]?.jsonObject, + ), + ) + } + NwcNotificationType.HOLD_INVOICE_ACCEPTED -> { + val notifObj = jsonObject["notification"]?.jsonObject + HoldInvoiceAcceptedNotification( + notification = + notifObj?.let { + HoldInvoiceAcceptedData( + type = it["type"]?.jsonPrimitive?.content, + invoice = it["invoice"]?.jsonPrimitive?.content, + payment_hash = it["payment_hash"]?.jsonPrimitive?.content, + amount = it["amount"]?.jsonPrimitive?.longOrNull, + created_at = it["created_at"]?.jsonPrimitive?.longOrNull, + expires_at = it["expires_at"]?.jsonPrimitive?.longOrNull, + settle_deadline = it["settle_deadline"]?.jsonPrimitive?.longOrNull, + ) + }, + ) + } + else -> throw IllegalArgumentException("Unknown notification type: $notificationType") + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47RequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47RequestKSerializer.kt new file mode 100644 index 0000000000..4bf4f67c6b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47RequestKSerializer.kt @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceParams +import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionMethod +import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionParams +import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetMethod +import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoMethod +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsParams +import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceParams +import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceParams +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceParams +import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceParams +import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendMethod +import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendParams +import com.vitorpamplona.quartz.nip47WalletConnect.Request +import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceParams +import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageMethod +import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageParams +import com.vitorpamplona.quartz.nip47WalletConnect.TlvRecord +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull + +object Nip47RequestKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Nip47Request") + + override fun serialize( + encoder: Encoder, + value: Request, + ) { + throw UnsupportedOperationException("NIP-47 Request serialization not supported") + } + + override fun deserialize(decoder: Decoder): Request { + val jsonDecoder = decoder as JsonDecoder + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + val method = jsonObject["method"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content } + + return when (method) { + NwcMethod.PAY_INVOICE -> parsePayInvoice(jsonObject) + NwcMethod.PAY_KEYSEND -> parsePayKeysend(jsonObject) + NwcMethod.MAKE_INVOICE -> parseMakeInvoice(jsonObject) + NwcMethod.LOOKUP_INVOICE -> parseLookupInvoice(jsonObject) + NwcMethod.LIST_TRANSACTIONS -> parseListTransactions(jsonObject) + NwcMethod.GET_BALANCE -> GetBalanceMethod() + NwcMethod.GET_INFO -> GetInfoMethod() + NwcMethod.GET_BUDGET -> GetBudgetMethod() + NwcMethod.SIGN_MESSAGE -> parseSignMessage(jsonObject) + NwcMethod.CREATE_CONNECTION -> parseCreateConnection(jsonObject) + NwcMethod.MAKE_HOLD_INVOICE -> parseMakeHoldInvoice(jsonObject) + NwcMethod.CANCEL_HOLD_INVOICE -> parseCancelHoldInvoice(jsonObject) + NwcMethod.SETTLE_HOLD_INVOICE -> parseSettleHoldInvoice(jsonObject) + else -> throw IllegalArgumentException("Unknown NWC method: $method") + } + } + + private fun parsePayInvoice(json: JsonObject): PayInvoiceMethod { + val params = json["params"]?.jsonObject + return PayInvoiceMethod( + params?.let { + PayInvoiceParams( + invoice = it["invoice"]?.jsonPrimitive?.content, + amount = it["amount"]?.jsonPrimitive?.longOrNull, + ) + }, + ) + } + + private fun parsePayKeysend(json: JsonObject): PayKeysendMethod { + val params = json["params"]?.jsonObject + return PayKeysendMethod( + params?.let { + PayKeysendParams( + amount = it["amount"]?.jsonPrimitive?.longOrNull, + pubkey = it["pubkey"]?.jsonPrimitive?.content, + preimage = it["preimage"]?.jsonPrimitive?.content, + tlv_records = + it["tlv_records"]?.jsonArray?.map { record -> + val obj = record.jsonObject + TlvRecord( + type = obj["type"]?.jsonPrimitive?.longOrNull, + value = obj["value"]?.jsonPrimitive?.content, + ) + }, + ) + }, + ) + } + + private fun parseMakeInvoice(json: JsonObject): MakeInvoiceMethod { + val params = json["params"]?.jsonObject + return MakeInvoiceMethod( + params?.let { + MakeInvoiceParams( + amount = it["amount"]?.jsonPrimitive?.longOrNull, + description = it["description"]?.jsonPrimitive?.content, + description_hash = it["description_hash"]?.jsonPrimitive?.content, + expiry = it["expiry"]?.jsonPrimitive?.longOrNull, + ) + }, + ) + } + + private fun parseLookupInvoice(json: JsonObject): LookupInvoiceMethod { + val params = json["params"]?.jsonObject + return LookupInvoiceMethod( + params?.let { + LookupInvoiceParams( + payment_hash = it["payment_hash"]?.jsonPrimitive?.content, + invoice = it["invoice"]?.jsonPrimitive?.content, + ) + }, + ) + } + + private fun parseListTransactions(json: JsonObject): ListTransactionsMethod { + val params = json["params"]?.jsonObject + return ListTransactionsMethod( + params?.let { + ListTransactionsParams( + from = it["from"]?.jsonPrimitive?.longOrNull, + until = it["until"]?.jsonPrimitive?.longOrNull, + limit = it["limit"]?.jsonPrimitive?.intOrNull, + offset = it["offset"]?.jsonPrimitive?.intOrNull, + unpaid = it["unpaid"]?.jsonPrimitive?.booleanOrNull, + unpaid_outgoing = it["unpaid_outgoing"]?.jsonPrimitive?.booleanOrNull, + unpaid_incoming = it["unpaid_incoming"]?.jsonPrimitive?.booleanOrNull, + type = it["type"]?.jsonPrimitive?.content, + ) + }, + ) + } + + private fun parseSignMessage(json: JsonObject): SignMessageMethod { + val params = json["params"]?.jsonObject + return SignMessageMethod( + params?.let { + SignMessageParams( + message = it["message"]?.jsonPrimitive?.content, + ) + }, + ) + } + + private fun parseCreateConnection(json: JsonObject): CreateConnectionMethod { + val params = json["params"]?.jsonObject + return CreateConnectionMethod( + params?.let { + CreateConnectionParams( + pubkey = it["pubkey"]?.jsonPrimitive?.content, + name = it["name"]?.jsonPrimitive?.content, + request_methods = it["request_methods"]?.jsonArray?.map { m -> m.jsonPrimitive.content }, + notification_types = it["notification_types"]?.jsonArray?.map { n -> n.jsonPrimitive.content }, + max_amount = it["max_amount"]?.jsonPrimitive?.longOrNull, + budget_renewal = it["budget_renewal"]?.jsonPrimitive?.content, + expires_at = it["expires_at"]?.jsonPrimitive?.longOrNull, + isolated = it["isolated"]?.jsonPrimitive?.booleanOrNull, + ) + }, + ) + } + + private fun parseMakeHoldInvoice(json: JsonObject): MakeHoldInvoiceMethod { + val params = json["params"]?.jsonObject + return MakeHoldInvoiceMethod( + params?.let { + MakeHoldInvoiceParams( + amount = it["amount"]?.jsonPrimitive?.longOrNull, + description = it["description"]?.jsonPrimitive?.content, + description_hash = it["description_hash"]?.jsonPrimitive?.content, + expiry = it["expiry"]?.jsonPrimitive?.longOrNull, + payment_hash = it["payment_hash"]?.jsonPrimitive?.content, + min_cltv_expiry_delta = it["min_cltv_expiry_delta"]?.jsonPrimitive?.intOrNull, + ) + }, + ) + } + + private fun parseCancelHoldInvoice(json: JsonObject): CancelHoldInvoiceMethod { + val params = json["params"]?.jsonObject + return CancelHoldInvoiceMethod( + params?.let { + CancelHoldInvoiceParams( + payment_hash = it["payment_hash"]?.jsonPrimitive?.content, + ) + }, + ) + } + + private fun parseSettleHoldInvoice(json: JsonObject): SettleHoldInvoiceMethod { + val params = json["params"]?.jsonObject + return SettleHoldInvoiceMethod( + params?.let { + SettleHoldInvoiceParams( + preimage = it["preimage"]?.jsonPrimitive?.content, + ) + }, + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47ResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47ResponseKSerializer.kt new file mode 100644 index 0000000000..b01c098a56 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47ResponseKSerializer.kt @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.NwcError +import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorCode +import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse +import com.vitorpamplona.quartz.nip47WalletConnect.NwcMethod +import com.vitorpamplona.quartz.nip47WalletConnect.NwcTransaction +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.Response +import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageSuccessResponse +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.longOrNull + +object Nip47ResponseKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Nip47Response") + + override fun serialize( + encoder: Encoder, + value: Response, + ) { + throw UnsupportedOperationException("NIP-47 Response serialization not supported") + } + + override fun deserialize(decoder: Decoder): Response { + val jsonDecoder = decoder as JsonDecoder + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + val resultType = jsonObject["result_type"]?.let { if (it is JsonNull) null else it.jsonPrimitive.content } + val hasError = jsonObject["error"]?.let { it !is JsonNull } ?: false + val hasResult = jsonObject["result"]?.let { it !is JsonNull } ?: false + + if (hasError) { + return when (resultType) { + NwcMethod.PAY_INVOICE -> parsePayInvoiceError(jsonObject) + else -> { + val error = jsonObject["error"]?.jsonObject?.let { parseNwcError(it) } + NwcErrorResponse(resultType ?: "", error) + } + } + } + + if (hasResult || resultType != null) { + return when (resultType) { + NwcMethod.PAY_INVOICE -> parsePayInvoiceSuccess(jsonObject) + NwcMethod.PAY_KEYSEND -> parsePayKeysendSuccess(jsonObject) + NwcMethod.MAKE_INVOICE -> MakeInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + NwcMethod.LOOKUP_INVOICE -> LookupInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + NwcMethod.LIST_TRANSACTIONS -> parseListTransactionsSuccess(jsonObject) + NwcMethod.GET_BALANCE -> parseGetBalanceSuccess(jsonObject) + NwcMethod.GET_INFO -> parseGetInfoSuccess(jsonObject) + NwcMethod.GET_BUDGET -> parseGetBudgetSuccess(jsonObject) + NwcMethod.SIGN_MESSAGE -> parseSignMessageSuccess(jsonObject) + NwcMethod.CREATE_CONNECTION -> parseCreateConnectionSuccess(jsonObject) + NwcMethod.MAKE_HOLD_INVOICE -> MakeHoldInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + NwcMethod.CANCEL_HOLD_INVOICE -> CancelHoldInvoiceSuccessResponse() + NwcMethod.SETTLE_HOLD_INVOICE -> SettleHoldInvoiceSuccessResponse() + else -> { + // backward compatibility: guess by result content + val resultObj = jsonObject["result"]?.jsonObject + if (resultObj?.containsKey("preimage") == true) { + return parsePayInvoiceSuccess(jsonObject) + } + throw IllegalArgumentException("Unknown NWC response type: $resultType") + } + } + } + + throw IllegalArgumentException("NWC response has neither result nor error") + } + + private fun parseNwcError(obj: JsonObject): NwcError { + val code = obj["code"]?.jsonPrimitive?.content?.let { codeName -> + try { + NwcErrorCode.valueOf(codeName) + } catch (_: Exception) { + null + } + } + return NwcError(code, obj["message"]?.jsonPrimitive?.content) + } + + fun parseTransaction(obj: JsonObject?): NwcTransaction? { + if (obj == null) return null + return NwcTransaction( + type = obj["type"]?.jsonPrimitive?.content, + state = obj["state"]?.jsonPrimitive?.content, + invoice = obj["invoice"]?.jsonPrimitive?.content, + description = obj["description"]?.jsonPrimitive?.content, + description_hash = obj["description_hash"]?.jsonPrimitive?.content, + preimage = obj["preimage"]?.jsonPrimitive?.content, + payment_hash = obj["payment_hash"]?.jsonPrimitive?.content, + amount = obj["amount"]?.jsonPrimitive?.longOrNull, + fees_paid = obj["fees_paid"]?.jsonPrimitive?.longOrNull, + created_at = obj["created_at"]?.jsonPrimitive?.longOrNull, + expires_at = obj["expires_at"]?.jsonPrimitive?.longOrNull, + settled_at = obj["settled_at"]?.jsonPrimitive?.longOrNull, + settle_deadline = obj["settle_deadline"]?.jsonPrimitive?.longOrNull, + ) + } + + private fun parsePayInvoiceSuccess(json: JsonObject): PayInvoiceSuccessResponse { + val result = json["result"]?.jsonObject + return PayInvoiceSuccessResponse( + result?.let { + PayInvoiceSuccessResponse.PayInvoiceResultParams( + preimage = it["preimage"]?.jsonPrimitive?.content, + fees_paid = it["fees_paid"]?.jsonPrimitive?.longOrNull, + ) + }, + ) + } + + private fun parsePayInvoiceError(json: JsonObject): PayInvoiceErrorResponse { + val error = json["error"]?.jsonObject + return PayInvoiceErrorResponse( + error?.let { + PayInvoiceErrorResponse.PayInvoiceErrorParams( + code = it["code"]?.jsonPrimitive?.content?.let { codeName -> + try { + NwcErrorCode.valueOf(codeName) + } catch (_: Exception) { + null + } + }, + message = it["message"]?.jsonPrimitive?.content, + ) + }, + ) + } + + private fun parsePayKeysendSuccess(json: JsonObject): PayKeysendSuccessResponse { + val result = json["result"]?.jsonObject + return PayKeysendSuccessResponse( + result?.let { + PayKeysendSuccessResponse.PayKeysendResult( + preimage = it["preimage"]?.jsonPrimitive?.content, + fees_paid = it["fees_paid"]?.jsonPrimitive?.longOrNull, + ) + }, + ) + } + + private fun parseListTransactionsSuccess(json: JsonObject): ListTransactionsSuccessResponse { + val result = json["result"]?.jsonObject + return ListTransactionsSuccessResponse( + result?.let { + ListTransactionsSuccessResponse.ListTransactionsResult( + transactions = it["transactions"]?.jsonArray?.map { t -> parseTransaction(t.jsonObject) }, + total_count = it["total_count"]?.jsonPrimitive?.longOrNull, + ) + }, + ) + } + + private fun parseGetBalanceSuccess(json: JsonObject): GetBalanceSuccessResponse { + val result = json["result"]?.jsonObject + return GetBalanceSuccessResponse( + result?.let { + GetBalanceSuccessResponse.GetBalanceResult( + balance = it["balance"]?.jsonPrimitive?.longOrNull, + ) + }, + ) + } + + private fun parseGetInfoSuccess(json: JsonObject): GetInfoSuccessResponse { + val result = json["result"]?.jsonObject + return GetInfoSuccessResponse( + result?.let { + GetInfoSuccessResponse.GetInfoResult( + alias = it["alias"]?.jsonPrimitive?.content, + color = it["color"]?.jsonPrimitive?.content, + pubkey = it["pubkey"]?.jsonPrimitive?.content, + network = it["network"]?.jsonPrimitive?.content, + block_height = it["block_height"]?.jsonPrimitive?.longOrNull, + block_hash = it["block_hash"]?.jsonPrimitive?.content, + methods = it["methods"]?.jsonArray?.map { m -> m.jsonPrimitive.content }, + notifications = it["notifications"]?.jsonArray?.map { n -> n.jsonPrimitive.content }, + lud16 = it["lud16"]?.jsonPrimitive?.content, + ) + }, + ) + } + + private fun parseGetBudgetSuccess(json: JsonObject): GetBudgetSuccessResponse { + val result = json["result"]?.jsonObject + return GetBudgetSuccessResponse( + result?.let { + GetBudgetSuccessResponse.GetBudgetResult( + used_budget = it["used_budget"]?.jsonPrimitive?.longOrNull, + total_budget = it["total_budget"]?.jsonPrimitive?.longOrNull, + renews_at = it["renews_at"]?.jsonPrimitive?.longOrNull, + renewal_period = it["renewal_period"]?.jsonPrimitive?.content, + ) + }, + ) + } + + private fun parseSignMessageSuccess(json: JsonObject): SignMessageSuccessResponse { + val result = json["result"]?.jsonObject + return SignMessageSuccessResponse( + result?.let { + SignMessageSuccessResponse.SignMessageResult( + message = it["message"]?.jsonPrimitive?.content, + signature = it["signature"]?.jsonPrimitive?.content, + ) + }, + ) + } + + private fun parseCreateConnectionSuccess(json: JsonObject): CreateConnectionSuccessResponse { + val result = json["result"]?.jsonObject + return CreateConnectionSuccessResponse( + result?.let { + CreateConnectionSuccessResponse.CreateConnectionResult( + wallet_pubkey = it["wallet_pubkey"]?.jsonPrimitive?.content, + ) + }, + ) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/RumorKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/RumorKSerializer.kt new file mode 100644 index 0000000000..400eb9017e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/RumorKSerializer.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.int +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import kotlinx.serialization.json.put + +object RumorKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("Rumor") { + element("id") + element("pubkey") + element("created_at") + element("kind") + element("tags", TagArrayKSerializer.descriptor) + element("content") + } + + override fun serialize( + encoder: Encoder, + value: Rumor, + ) { + val jsonEncoder = encoder as JsonEncoder + val element = + buildJsonObject { + value.id?.let { put("id", it) } + value.pubKey?.let { put("pubkey", it) } + value.createdAt?.let { put("created_at", it) } + value.kind?.let { put("kind", it) } + value.tags?.let { put("tags", TagArrayKSerializer.serializeToElement(it)) } + value.content?.let { put("content", it) } + } + jsonEncoder.encodeJsonElement(element) + } + + override fun deserialize(decoder: Decoder): Rumor { + val jsonDecoder = decoder as JsonDecoder + val jsonObject = jsonDecoder.decodeJsonElement().jsonObject + + var id: HexKey? = null + var pubKey: HexKey? = null + var createdAt: Long? = null + var kind: Int? = null + var tags: TagArray? = null + var content: String? = null + + for ((key, value) in jsonObject) { + when (key) { + "id" -> id = value.jsonPrimitive.content + "pubkey" -> pubKey = value.jsonPrimitive.content + "created_at" -> createdAt = value.jsonPrimitive.long + "kind" -> kind = value.jsonPrimitive.int + "tags" -> tags = TagArrayKSerializer.deserializeFromElement(value) + "content" -> content = value.jsonPrimitive.content + } + } + + return Rumor(id, pubKey, createdAt, kind, tags, content) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt new file mode 100644 index 0000000000..3e6562808d --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonPrimitive + +object TagArrayKSerializer : KSerializer { + override val descriptor: SerialDescriptor = + ListSerializer(ListSerializer(String.serializer())).descriptor + + override fun serialize( + encoder: Encoder, + value: TagArray, + ) { + val jsonEncoder = encoder as JsonEncoder + val element = + buildJsonArray { + for (tag in value) { + add( + buildJsonArray { + for (s in tag) { + add(JsonPrimitive(s)) + } + }, + ) + } + } + jsonEncoder.encodeJsonElement(element) + } + + override fun deserialize(decoder: Decoder): TagArray { + val jsonDecoder = decoder as JsonDecoder + return deserializeFromElement(jsonDecoder.decodeJsonElement()) + } + + fun deserializeFromElement(element: JsonElement): TagArray { + val array = element.jsonArray + val outerList = ArrayList>(array.size) + for (inner in array) { + val innerArray = inner.jsonArray + val innerList = ArrayList(innerArray.size.coerceAtLeast(5)) + for (s in innerArray) { + if (s is JsonNull) { + innerList.add("") + } else { + innerList.add(s.jsonPrimitive.content) + } + } + @Suppress("UNCHECKED_CAST") + outerList.add(innerList.toArray(arrayOfNulls(innerList.size)) as Array) + } + @Suppress("UNCHECKED_CAST") + return outerList.toArray(arrayOfNulls>(outerList.size)) as TagArray + } + + fun serializeToElement(value: TagArray): JsonArray = + buildJsonArray { + for (tag in value) { + add( + buildJsonArray { + for (s in tag) { + add(JsonPrimitive(s)) + } + }, + ) + } + } +} diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt new file mode 100644 index 0000000000..2520242a04 --- /dev/null +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt @@ -0,0 +1,740 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountResult +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NoticeMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.NotifyMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.OkMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CloseCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.EventCmd +import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.ReqCmd +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse +import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class KotlinSerializationMapperTest { + val tags = + arrayOf( + arrayOf("title", "Retro Computer Fans"), + arrayOf("d", "xmbspe8rddsq"), + arrayOf("image", "https://blog.johnnovak.net/2022/04/15/achieving-period-correct-graphics-in-personal-computer-emulators-part-1-the-amiga/img/dream-setup.jpg"), + arrayOf("p", "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da"), + arrayOf("p", "9a9a4aa0e43e57873380ab22e8a3df12f3c4cf5bb3a804c6e3fed0069a6e2740"), + arrayOf("p", "4f5dd82517b11088ce00f23d99f06fe8f3e2e45ecf47bc9c2f90f34d5c6f7382"), + arrayOf("p", "ac92102a2ecb873c488e0125354ef5a97075a16198668c360eda050007ed42cd"), + arrayOf("p", "47f54409a4620eb35208a3bc1b53555bf3d0656b246bf0471a93208e20672f6f"), + arrayOf("p", "2624911545afb7a2b440cf10f5c69308afa33aae26fca664d8c94623dc0f1baf"), + arrayOf("p", "6641f26f5c59f7010dbe3e42e4593398e27c087497cb7d20e0e7633a17e48a94"), + arrayOf("description", "Retro computer fans and enthusiasts "), + ) + + val followCard = + FollowListEvent( + id = "eca31634fce7c9068b56fa8db9f387da70bdcceb3986a77ca1a9844f3128eb5f", + pubKey = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da", + createdAt = 1761736286, + tags = tags, + content = "", + sig = "3aa388edafad151e81cb0228fe04e115dbbcaa851c666bfe3c8740b6cd99575f0fc3ba2d47acda86f7626564a05e9dbc05ef452a7bd0ac00f828dbad0e1bae6c", + ) + + val followCardRumor = + Rumor( + id = followCard.id, + pubKey = followCard.pubKey, + createdAt = followCard.createdAt, + kind = followCard.kind, + tags = followCard.tags, + content = followCard.content, + ) + + val followCardTemplate = + EventTemplate( + createdAt = followCard.createdAt, + kind = followCard.kind, + tags = followCard.tags, + content = followCard.content, + ) + + // ========================================================================= + // TagArray Tests + // ========================================================================= + + @Test + fun serializeTagArray_matchesJackson() { + val jacksonJson = JacksonMapper.toJson(tags) + val kotlinJson = KotlinSerializationMapper.toJson(tags) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeTagArray_matchesJackson() { + val json = JacksonMapper.toJson(tags) + val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json) + tags.forEachIndexed { index, tag -> + assertContentEquals(tag, deserialized[index]) + } + } + + @Test + fun tagArrayRoundTrip() { + val json = KotlinSerializationMapper.toJson(tags) + val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json) + assertEquals(tags.size, deserialized.size) + tags.forEachIndexed { index, tag -> + assertContentEquals(tag, deserialized[index]) + } + } + + @Test + fun tagArrayWithNullValues() { + val json = """[["key",null,"value"]]""" + val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json) + assertEquals(1, deserialized.size) + assertEquals("key", deserialized[0][0]) + assertEquals("", deserialized[0][1]) // null -> "" + assertEquals("value", deserialized[0][2]) + } + + @Test + fun emptyTagArray() { + val json = "[]" + val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json) + assertEquals(0, deserialized.size) + } + + // ========================================================================= + // Event Tests + // ========================================================================= + + @Test + fun serializeEvent_matchesJackson() { + val jacksonJson = JacksonMapper.toJson(followCard) + val kotlinJson = KotlinSerializationMapper.toJson(followCard) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeEvent_matchesJackson() { + val json = JacksonMapper.toJson(followCard) + val deserialized = KotlinSerializationMapper.fromJson(json) + + assertEquals(followCard.id, deserialized.id) + assertEquals(followCard.pubKey, deserialized.pubKey) + assertEquals(followCard.createdAt, deserialized.createdAt) + assertEquals(followCard.kind, deserialized.kind) + assertEquals(followCard.content, deserialized.content) + assertEquals(followCard.sig, deserialized.sig) + tags.forEachIndexed { index, tag -> + assertContentEquals(tag, deserialized.tags[index]) + } + } + + @Test + fun eventRoundTrip() { + val json = KotlinSerializationMapper.toJson(followCard) + val deserialized = KotlinSerializationMapper.fromJson(json) + + assertEquals(followCard.id, deserialized.id) + assertEquals(followCard.kind, deserialized.kind) + assertEquals(followCard.createdAt, deserialized.createdAt) + assertEquals(followCard.pubKey, deserialized.pubKey) + assertEquals(followCard.content, deserialized.content) + assertEquals(followCard.sig, deserialized.sig) + } + + @Test + fun deserializeEventWithUnknownFields() { + val json = + """{"id":"abc123","pubkey":"def456","created_at":12345,"kind":1,"tags":[],"content":"test","sig":"sig123","unknown_field":"ignored"}""" + // Should not throw with unknown fields, should be ignored + val deserialized = KotlinSerializationMapper.fromJson(json) + assertEquals("abc123", deserialized.id) + assertEquals("def456", deserialized.pubKey) + assertEquals("test", deserialized.content) + } + + @Test + fun deserializeEventWithSpecialCharactersInContent() { + val content = "Hello \"world\" \n\ttab\\backslash" + val event = + FollowListEvent( + id = "abc", + pubKey = "def", + createdAt = 1000, + tags = emptyArray(), + content = content, + sig = "sig", + ) + val json = KotlinSerializationMapper.toJson(event) + val deserialized = KotlinSerializationMapper.fromJson(json) + assertEquals(content, deserialized.content) + } + + @Test + fun crossDeserializationEvent() { + // Serialize with Jackson, deserialize with Kotlin Serialization + val jacksonJson = JacksonMapper.toJson(followCard) + val kotlinDeserialized = KotlinSerializationMapper.fromJson(jacksonJson) + + assertEquals(followCard.id, kotlinDeserialized.id) + assertEquals(followCard.pubKey, kotlinDeserialized.pubKey) + assertEquals(followCard.kind, kotlinDeserialized.kind) + + // Serialize with Kotlin Serialization, deserialize with Jackson + val kotlinJson = KotlinSerializationMapper.toJson(followCard) + val jacksonDeserialized = JacksonMapper.fromJson(kotlinJson) + + assertEquals(followCard.id, jacksonDeserialized.id) + assertEquals(followCard.pubKey, jacksonDeserialized.pubKey) + assertEquals(followCard.kind, jacksonDeserialized.kind) + } + + // ========================================================================= + // Rumor Tests + // ========================================================================= + + @Test + fun serializeRumor_matchesJackson() { + val jacksonJson = JacksonMapper.toJson(followCardRumor) + val kotlinJson = KotlinSerializationMapper.toJson(followCardRumor) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeRumor_matchesJackson() { + val json = JacksonMapper.toJson(followCardRumor) + val deserialized = KotlinSerializationMapper.fromJsonToRumor(json) + + assertEquals(followCardRumor.id, deserialized.id) + assertEquals(followCardRumor.pubKey, deserialized.pubKey) + assertEquals(followCardRumor.createdAt, deserialized.createdAt) + assertEquals(followCardRumor.kind, deserialized.kind) + assertEquals(followCardRumor.content, deserialized.content) + } + + @Test + fun rumorRoundTrip() { + val json = KotlinSerializationMapper.toJson(followCardRumor) + val deserialized = KotlinSerializationMapper.fromJsonToRumor(json) + + assertEquals(followCardRumor.id, deserialized.id) + assertEquals(followCardRumor.kind, deserialized.kind) + } + + @Test + fun rumorWithNullFields() { + val rumor = Rumor(null, null, null, null, null, null) + val json = KotlinSerializationMapper.toJson(rumor) + assertEquals("{}", json) + + val deserialized = KotlinSerializationMapper.fromJsonToRumor(json) + assertNull(deserialized.id) + assertNull(deserialized.pubKey) + assertNull(deserialized.createdAt) + assertNull(deserialized.kind) + assertNull(deserialized.tags) + assertNull(deserialized.content) + } + + @Test + fun rumorPartialFields() { + val rumor = Rumor("abc", null, 1000, 1, null, "content") + val json = KotlinSerializationMapper.toJson(rumor) + val deserialized = KotlinSerializationMapper.fromJsonToRumor(json) + + assertEquals("abc", deserialized.id) + assertNull(deserialized.pubKey) + assertEquals(1000L, deserialized.createdAt) + assertEquals(1, deserialized.kind) + assertNull(deserialized.tags) + assertEquals("content", deserialized.content) + } + + // ========================================================================= + // EventTemplate Tests + // ========================================================================= + + @Test + fun serializeTemplate_matchesJackson() { + val jacksonJson = JacksonMapper.toJson(followCardTemplate) + val kotlinJson = KotlinSerializationMapper.toJson(followCardTemplate) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeTemplate_matchesJackson() { + val json = JacksonMapper.toJson(followCardTemplate) + val deserialized = KotlinSerializationMapper.fromJsonToEventTemplate(json) + + assertEquals(followCardTemplate.kind, deserialized.kind) + assertEquals(followCardTemplate.createdAt, deserialized.createdAt) + assertEquals(followCardTemplate.content, deserialized.content) + tags.forEachIndexed { index, tag -> + assertContentEquals(tag, deserialized.tags[index]) + } + } + + @Test + fun templateRoundTrip() { + val json = KotlinSerializationMapper.toJson(followCardTemplate) + val deserialized = KotlinSerializationMapper.fromJsonToEventTemplate(json) + + assertEquals(followCardTemplate.kind, deserialized.kind) + assertEquals(followCardTemplate.createdAt, deserialized.createdAt) + assertEquals(followCardTemplate.content, deserialized.content) + } + + @Test + fun templateWithDifferentFieldOrder() { + // Fields in different order than expected + val json = """{"kind":1,"content":"test","created_at":1234,"tags":[]}""" + val deserialized = KotlinSerializationMapper.fromJsonToEventTemplate(json) + assertEquals(1, deserialized.kind) + assertEquals("test", deserialized.content) + assertEquals(1234L, deserialized.createdAt) + } + + // ========================================================================= + // Filter Tests + // ========================================================================= + + @Test + fun emptyFilter_matchesJackson() { + val filter = Filter() + val jacksonJson = JacksonMapper.toJson(filter) + val kotlinJson = KotlinSerializationMapper.toJson(filter) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun filterWithAllFields_matchesJackson() { + val filter = + Filter( + ids = listOf("abc123" + "0".repeat(58)), + authors = listOf("def456" + "0".repeat(58)), + kinds = listOf(1, 2, 3), + tags = mapOf("p" to listOf("3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da")), + tagsAll = mapOf("p" to listOf("3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da")), + since = 1000L, + until = 2000L, + limit = 50, + search = "hello", + ) + val jacksonJson = JacksonMapper.toJson(filter) + val kotlinJson = KotlinSerializationMapper.toJson(filter) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun filterRoundTrip() { + val expectedTagValue = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da" + val filter = + Filter( + tags = mapOf("p" to listOf(expectedTagValue)), + tagsAll = mapOf("p" to listOf(expectedTagValue)), + ) + val json = KotlinSerializationMapper.toJson(filter) + val deserialized = KotlinSerializationMapper.fromJsonTo(json) + + assertEquals(true, deserialized.tags?.keys?.contains("p")) + assertEquals(listOf(expectedTagValue), deserialized.tags?.get("p")) + assertEquals(true, deserialized.tagsAll?.keys?.contains("p")) + assertEquals(listOf(expectedTagValue), deserialized.tagsAll?.get("p")) + } + + @Test + fun deserializeEmptyFilter() { + val json = Filter().toJson() + val deserialized = KotlinSerializationMapper.fromJsonTo(json) + assertNull(deserialized.ids) + } + + @Test + fun crossDeserializationFilter() { + val expectedTagValue = "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da" + val filter = + Filter( + tags = mapOf("p" to listOf(expectedTagValue)), + tagsAll = mapOf("p" to listOf(expectedTagValue)), + ) + + // Jackson serialized -> Kotlin deserialized + val jacksonJson = JacksonMapper.toJson(filter) + val kotlinDeserialized = KotlinSerializationMapper.fromJsonTo(jacksonJson) + assertEquals(listOf(expectedTagValue), kotlinDeserialized.tags?.get("p")) + assertEquals(listOf(expectedTagValue), kotlinDeserialized.tagsAll?.get("p")) + + // Kotlin serialized -> Jackson deserialized + val kotlinJson = KotlinSerializationMapper.toJson(filter) + val jacksonDeserialized = JacksonMapper.fromJsonTo(kotlinJson) + assertEquals(listOf(expectedTagValue), jacksonDeserialized.tags?.get("p")) + assertEquals(listOf(expectedTagValue), jacksonDeserialized.tagsAll?.get("p")) + } + + // ========================================================================= + // Message Tests + // ========================================================================= + + @Test + fun serializeEventMessage_matchesJackson() { + val msg = EventMessage("sub1", followCard) + val jacksonJson = JacksonMapper.toJson(msg) + val kotlinJson = KotlinSerializationMapper.toJson(msg) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeEventMessage() { + val msg = EventMessage("sub1", followCard) + val json = KotlinSerializationMapper.toJson(msg) + val deserialized = KotlinSerializationMapper.fromJsonToMessage(json) + + assertTrue(deserialized is EventMessage) + assertEquals("sub1", deserialized.subId) + assertEquals(followCard.id, deserialized.event.id) + } + + @Test + fun serializeNoticeMessage_matchesJackson() { + val msg = NoticeMessage("something went wrong") + val jacksonJson = JacksonMapper.toJson(msg) + val kotlinJson = KotlinSerializationMapper.toJson(msg) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun serializeOkMessage_matchesJackson() { + val msg = OkMessage("abc123", true, "success") + val jacksonJson = JacksonMapper.toJson(msg) + val kotlinJson = KotlinSerializationMapper.toJson(msg) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeOkMessage() { + val msg = OkMessage("abc123", false, "rate limited") + val json = KotlinSerializationMapper.toJson(msg) + val deserialized = KotlinSerializationMapper.fromJsonToMessage(json) + + assertTrue(deserialized is OkMessage) + assertEquals("abc123", deserialized.eventId) + assertEquals(false, deserialized.success) + assertEquals("rate limited", deserialized.message) + } + + @Test + fun serializeAuthMessage_matchesJackson() { + val msg = AuthMessage("challenge123") + val jacksonJson = JacksonMapper.toJson(msg) + val kotlinJson = KotlinSerializationMapper.toJson(msg) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun serializeNotifyMessage_matchesJackson() { + val msg = NotifyMessage("notification text") + val jacksonJson = JacksonMapper.toJson(msg) + val kotlinJson = KotlinSerializationMapper.toJson(msg) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun serializeClosedMessage_matchesJackson() { + val msg = ClosedMessage("sub1", "subscription closed") + val jacksonJson = JacksonMapper.toJson(msg) + val kotlinJson = KotlinSerializationMapper.toJson(msg) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeEoseMessage() { + val json = """["EOSE","sub123"]""" + val deserialized = KotlinSerializationMapper.fromJsonToMessage(json) + assertTrue(deserialized is EoseMessage) + assertEquals("sub123", (deserialized as EoseMessage).subId) + } + + @Test + fun serializeCountMessage_matchesJackson() { + val msg = CountMessage("q1", CountResult(42, false)) + val jacksonJson = JacksonMapper.toJson(msg) + val kotlinJson = KotlinSerializationMapper.toJson(msg) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun crossDeserializationMessages() { + val messages = + listOf( + NoticeMessage("test"), + AuthMessage("challenge"), + NotifyMessage("notify"), + ClosedMessage("sub1", "reason"), + ) + + for (msg in messages) { + val jacksonJson = JacksonMapper.toJson(msg) + val kotlinDeserialized = KotlinSerializationMapper.fromJsonToMessage(jacksonJson) + assertEquals(msg.label(), kotlinDeserialized.label()) + + val kotlinJson = KotlinSerializationMapper.toJson(msg) + val jacksonDeserialized = JacksonMapper.fromJsonToMessage(kotlinJson) + assertEquals(msg.label(), jacksonDeserialized.label()) + } + } + + // ========================================================================= + // Command Tests + // ========================================================================= + + @Test + fun serializeReqCmd_matchesJackson() { + val filter = + Filter( + kinds = listOf(1), + limit = 10, + ) + val cmd = ReqCmd("sub1", listOf(filter)) + val jacksonJson = JacksonMapper.toJson(cmd) + val kotlinJson = KotlinSerializationMapper.toJson(cmd) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeReqCmd() { + val filter = Filter(kinds = listOf(1), limit = 10) + val cmd = ReqCmd("sub1", listOf(filter)) + val json = KotlinSerializationMapper.toJson(cmd) + val deserialized = KotlinSerializationMapper.fromJsonToCommand(json) + + assertTrue(deserialized is ReqCmd) + assertEquals("sub1", deserialized.subId) + assertEquals(1, deserialized.filters.size) + assertEquals(listOf(1), deserialized.filters[0].kinds) + assertEquals(10, deserialized.filters[0].limit) + } + + @Test + fun serializeEventCmd_matchesJackson() { + val cmd = EventCmd(followCard) + val jacksonJson = JacksonMapper.toJson(cmd) + val kotlinJson = KotlinSerializationMapper.toJson(cmd) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun serializeCloseCmd_matchesJackson() { + val cmd = CloseCmd("sub1") + val jacksonJson = JacksonMapper.toJson(cmd) + val kotlinJson = KotlinSerializationMapper.toJson(cmd) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun crossDeserializationCommands() { + val cmd = CloseCmd("sub1") + + val jacksonJson = JacksonMapper.toJson(cmd) + val kotlinDeserialized = KotlinSerializationMapper.fromJsonToCommand(jacksonJson) + assertTrue(kotlinDeserialized is CloseCmd) + assertEquals("sub1", (kotlinDeserialized as CloseCmd).subId) + + val kotlinJson = KotlinSerializationMapper.toJson(cmd) + val jacksonDeserialized = JacksonMapper.fromJsonToCommand(kotlinJson) + assertTrue(jacksonDeserialized is CloseCmd) + assertEquals("sub1", (jacksonDeserialized as CloseCmd).subId) + } + + // ========================================================================= + // BunkerRequest Tests + // ========================================================================= + + @Test + fun serializeBunkerRequest_matchesJackson() { + val req = BunkerRequest("id1", "connect", arrayOf("pubkey", "secret")) + val jacksonJson = JacksonMapper.toJson(req) + val kotlinJson = KotlinSerializationMapper.toJson(req) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeBunkerRequest() { + val json = """{"id":"id1","method":"ping","params":[]}""" + val deserialized = KotlinSerializationMapper.fromJsonTo(json) + assertEquals("id1", deserialized.id) + assertEquals("ping", deserialized.method) + } + + @Test + fun crossDeserializationBunkerRequest() { + val req = BunkerRequest("id1", "sign_event", arrayOf("eventJson")) + val jacksonJson = JacksonMapper.toJson(req) + val kotlinDeserialized = KotlinSerializationMapper.fromJsonTo(jacksonJson) + assertEquals(req.id, kotlinDeserialized.id) + assertEquals(req.method, kotlinDeserialized.method) + assertContentEquals(req.params, kotlinDeserialized.params) + + val kotlinJson = KotlinSerializationMapper.toJson(req) + val jacksonDeserialized = JacksonMapper.fromJsonTo(kotlinJson) + assertEquals(req.id, jacksonDeserialized.id) + assertEquals(req.method, jacksonDeserialized.method) + assertContentEquals(req.params, jacksonDeserialized.params) + } + + // ========================================================================= + // BunkerResponse Tests + // ========================================================================= + + @Test + fun serializeBunkerResponse_matchesJackson() { + val resp = BunkerResponse("id1", "ok", null) + val jacksonJson = JacksonMapper.toJson(resp) + val kotlinJson = KotlinSerializationMapper.toJson(resp) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun serializeBunkerResponseWithError_matchesJackson() { + val resp = BunkerResponse("id1", null, "something went wrong") + val jacksonJson = JacksonMapper.toJson(resp) + val kotlinJson = KotlinSerializationMapper.toJson(resp) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun deserializeBunkerResponse() { + val json = """{"id":"id1","result":"pong"}""" + val deserialized = KotlinSerializationMapper.fromJsonTo(json) + assertEquals("id1", deserialized.id) + assertNotNull(deserialized.result) + } + + // ========================================================================= + // OptimizedSerializable toJson dispatch Tests + // ========================================================================= + + @Test + fun toJsonDispatchForFilter() { + val filter = Filter(kinds = listOf(1)) + val jacksonJson = JacksonMapper.toJson(filter) + val kotlinJson = KotlinSerializationMapper.toJson(filter) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun toJsonDispatchForRumor() { + val jacksonJson = JacksonMapper.toJson(followCardRumor) + val kotlinJson = KotlinSerializationMapper.toJson(followCardRumor) + assertEquals(jacksonJson, kotlinJson) + } + + @Test + fun toJsonDispatchForEventTemplate() { + val jacksonJson = JacksonMapper.toJson(followCardTemplate) + val kotlinJson = KotlinSerializationMapper.toJson(followCardTemplate) + assertEquals(jacksonJson, kotlinJson) + } + + // ========================================================================= + // Edge Cases + // ========================================================================= + + @Test + fun emptyContentEvent() { + val event = + FollowListEvent( + id = "a".repeat(64), + pubKey = "b".repeat(64), + createdAt = 0, + tags = emptyArray(), + content = "", + sig = "c".repeat(64), + ) + val json = KotlinSerializationMapper.toJson(event) + val deserialized = KotlinSerializationMapper.fromJson(json) + assertEquals("", deserialized.content) + assertEquals(0, deserialized.tags.size) + } + + @Test + fun largeTagArray() { + val largeTags = Array(100) { i -> arrayOf("p", "key$i") } + val json = KotlinSerializationMapper.toJson(largeTags) + val deserialized = KotlinSerializationMapper.fromJsonToTagArray(json) + assertEquals(100, deserialized.size) + assertEquals("key99", deserialized[99][1]) + } + + @Test + fun eventWithUnicodeContent() { + val content = "Hello \uD83D\uDE00 world \u00E9\u00E8\u00EA" + val event = + FollowListEvent( + id = "a".repeat(64), + pubKey = "b".repeat(64), + createdAt = 1000, + tags = emptyArray(), + content = content, + sig = "c".repeat(64), + ) + val json = KotlinSerializationMapper.toJson(event) + val deserialized = KotlinSerializationMapper.fromJson(json) + assertEquals(content, deserialized.content) + } + + @Test + fun filterWithMultipleTagTypes() { + val filter = + Filter( + tags = + mapOf( + "p" to listOf("pubkey1", "pubkey2"), + "e" to listOf("eventid1"), + "t" to listOf("nostr", "bitcoin"), + ), + ) + val json = KotlinSerializationMapper.toJson(filter) + val deserialized = KotlinSerializationMapper.fromJsonTo(json) + + assertEquals(3, deserialized.tags?.size) + assertEquals(listOf("pubkey1", "pubkey2"), deserialized.tags?.get("p")) + assertEquals(listOf("eventid1"), deserialized.tags?.get("e")) + assertEquals(listOf("nostr", "bitcoin"), deserialized.tags?.get("t")) + } +} From 3bea2f795cee8d658e89ee3983cea0c683fe1fd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 19:55:19 +0000 Subject: [PATCH 109/172] refactor: Move KSerializers to their respective NIP packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bunker serializers → nip46RemoteSigner/kotlinSerialization/ - NIP-47 serializers → nip47WalletConnect/kotlinSerialization/ - Rumor serializer → nip59Giftwrap/rumors/kotlinSerialization/ Mirrors the existing Jackson serializer directory structure which uses a jackson/ subpackage within each NIP package. https://claude.ai/code/session_01QBc4Pb7a6m4TfLZSJKVdjw --- .../kotlinSerialization/KotlinSerializationMapper.kt | 7 +++++++ .../kotlinSerialization/BunkerMessageKSerializer.kt | 2 +- .../kotlinSerialization/BunkerRequestKSerializer.kt | 2 +- .../kotlinSerialization/BunkerResponseKSerializer.kt | 2 +- .../kotlinSerialization/Nip47NotificationKSerializer.kt | 2 +- .../kotlinSerialization/Nip47RequestKSerializer.kt | 2 +- .../kotlinSerialization/Nip47ResponseKSerializer.kt | 2 +- .../rumors}/kotlinSerialization/RumorKSerializer.kt | 4 +++- 8 files changed, 16 insertions(+), 7 deletions(-) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{nip01Core => nip46RemoteSigner}/kotlinSerialization/BunkerMessageKSerializer.kt (98%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{nip01Core => nip46RemoteSigner}/kotlinSerialization/BunkerRequestKSerializer.kt (98%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{nip01Core => nip46RemoteSigner}/kotlinSerialization/BunkerResponseKSerializer.kt (98%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{nip01Core => nip47WalletConnect}/kotlinSerialization/Nip47NotificationKSerializer.kt (98%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{nip01Core => nip47WalletConnect}/kotlinSerialization/Nip47RequestKSerializer.kt (99%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{nip01Core => nip47WalletConnect}/kotlinSerialization/Nip47ResponseKSerializer.kt (99%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/{nip01Core => nip59Giftwrap/rumors}/kotlinSerialization/RumorKSerializer.kt (96%) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt index 901bdf099b..086ed8951b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt @@ -30,10 +30,17 @@ import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse +import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerMessageKSerializer +import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerRequestKSerializer +import com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization.BunkerResponseKSerializer import com.vitorpamplona.quartz.nip47WalletConnect.Notification import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.Response +import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47NotificationKSerializer +import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47RequestKSerializer +import com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization.Nip47ResponseKSerializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor +import com.vitorpamplona.quartz.nip59Giftwrap.rumors.kotlinSerialization.RumorKSerializer import kotlinx.serialization.json.Json class KotlinSerializationMapper { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerMessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt similarity index 98% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerMessageKSerializer.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt index 157be1b957..97502f068c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerMessageKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.kotlinSerialization +package com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerRequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerRequestKSerializer.kt similarity index 98% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerRequestKSerializer.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerRequestKSerializer.kt index 3d748bfd0c..7797cf3371 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerRequestKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerRequestKSerializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.kotlinSerialization +package com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt similarity index 98% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerResponseKSerializer.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt index bf4cbbaf6e..1127255aae 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/BunkerResponseKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.kotlinSerialization +package com.vitorpamplona.quartz.nip46RemoteSigner.kotlinSerialization import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseAck diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47NotificationKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt similarity index 98% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47NotificationKSerializer.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt index 5ac1a412a6..8a105e1cff 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47NotificationKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.kotlinSerialization +package com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedData import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedNotification diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47RequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt similarity index 99% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47RequestKSerializer.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt index 4bf4f67c6b..0996ce75ac 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47RequestKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.kotlinSerialization +package com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceParams diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47ResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt similarity index 99% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47ResponseKSerializer.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt index b01c098a56..da7dfadc77 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/Nip47ResponseKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.kotlinSerialization +package com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionSuccessResponse diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/RumorKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt similarity index 96% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/RumorKSerializer.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt index 400eb9017e..a8f4d5956e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/RumorKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt @@ -18,7 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.nip01Core.kotlinSerialization +package com.vitorpamplona.quartz.nip59Giftwrap.rumors.kotlinSerialization + +import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.TagArrayKSerializer import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray From d10687d6fea7a8d0307a65f1cccc3bcdd4689485 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 19:58:40 +0000 Subject: [PATCH 110/172] feat: Implement iOS OptimizedJsonMapper using KotlinSerializationMapper Replace all TODO stubs in the iOS actual implementation with delegation to KotlinSerializationMapper. Wraps SerializationException into IllegalArgumentException to match the jvmAndroid error handling pattern. https://claude.ai/code/session_01QBc4Pb7a6m4TfLZSJKVdjw --- .../nip01Core/core/OptimizedJsonMapper.ios.kt | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.ios.kt index d77753912b..05e0bbbc0b 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/OptimizedJsonMapper.ios.kt @@ -20,29 +20,66 @@ */ package com.vitorpamplona.quartz.nip01Core.core +import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.KotlinSerializationMapper import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor +import kotlinx.serialization.SerializationException actual object OptimizedJsonMapper { - actual fun fromJson(json: String): Event = TODO("Not yet implemented") + actual fun fromJson(json: String): Event = + try { + KotlinSerializationMapper.fromJson(json) + } catch (e: SerializationException) { + throw IllegalArgumentException(e.message, e) + } - actual fun toJson(event: Event): String = TODO("Not yet implemented") + actual fun toJson(event: Event): String = KotlinSerializationMapper.toJson(event) - actual fun fromJsonToMessage(json: String): Message = TODO("Not yet implemented") + actual fun fromJsonToMessage(json: String): Message = + try { + KotlinSerializationMapper.fromJsonToMessage(json) + } catch (e: SerializationException) { + throw IllegalArgumentException(e.message, e) + } - actual fun fromJsonToCommand(json: String): Command = TODO("Not yet implemented") + actual fun fromJsonToCommand(json: String): Command = + try { + KotlinSerializationMapper.fromJsonToCommand(json) + } catch (e: SerializationException) { + throw IllegalArgumentException(e.message, e) + } - actual fun fromJsonToTagArray(json: String): Array> = TODO("Not yet implemented") + actual fun fromJsonToTagArray(json: String): Array> = + try { + KotlinSerializationMapper.fromJsonToTagArray(json) + } catch (e: SerializationException) { + throw IllegalArgumentException(e.message, e) + } - actual fun fromJsonToEventTemplate(json: String): EventTemplate = TODO("Not yet implemented") + actual fun fromJsonToEventTemplate(json: String): EventTemplate = + try { + KotlinSerializationMapper.fromJsonToEventTemplate(json) + } catch (e: SerializationException) { + throw IllegalArgumentException(e.message, e) + } - actual fun fromJsonToRumor(json: String): Rumor = TODO("Not yet implemented") + actual fun fromJsonToRumor(json: String): Rumor = + try { + KotlinSerializationMapper.fromJsonToRumor(json) + } catch (e: SerializationException) { + throw IllegalArgumentException(e.message, e) + } - actual fun toJson(tags: Array>): String = TODO("Not yet implemented") + actual fun toJson(tags: Array>): String = KotlinSerializationMapper.toJson(tags) - actual inline fun fromJsonTo(json: String): T = TODO("Not yet implemented") + actual inline fun fromJsonTo(json: String): T = + try { + KotlinSerializationMapper.fromJsonTo(json) + } catch (e: SerializationException) { + throw IllegalArgumentException(e.message, e) + } - actual fun toJson(value: OptimizedSerializable): String = TODO("Not yet implemented") + actual fun toJson(value: OptimizedSerializable): String = KotlinSerializationMapper.toJson(value) } From 30b76347c59f872c8ec3c6fa53738fd334ab5457 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 16:15:44 -0400 Subject: [PATCH 111/172] Spotless --- .../kotlinSerialization/CommandKSerializer.kt | 13 ++- .../kotlinSerialization/EventKSerializer.kt | 2 - .../kotlinSerialization/FilterKSerializer.kt | 4 +- .../KotlinSerializationMapper.kt | 45 ++++++-- .../kotlinSerialization/MessageKSerializer.kt | 18 ++- .../BunkerMessageKSerializer.kt | 72 ++++++++---- .../BunkerResponseKSerializer.kt | 10 +- .../Nip47NotificationKSerializer.kt | 11 +- .../Nip47RequestKSerializer.kt | 4 +- .../Nip47ResponseKSerializer.kt | 104 ++++++++++++------ .../kotlinSerialization/RumorKSerializer.kt | 3 +- 11 files changed, 206 insertions(+), 80 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt index 790a710ddf..4d3eba2998 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt @@ -59,15 +59,19 @@ object CommandKSerializer : KSerializer { add(FilterKSerializer.serializeToElement(filter)) } } + is EventCmd -> { add(EventKSerializer.serializeToElement(value.event)) } + is CloseCmd -> { add(JsonPrimitive(value.subId)) } + is AuthCmd -> { add(EventKSerializer.serializeToElement(value.event)) } + is CountCmd -> { add(JsonPrimitive(value.queryId)) for (filter in value.filters) { @@ -93,6 +97,7 @@ object CommandKSerializer : KSerializer { } ReqCmd(subId, filters) } + CountCmd.LABEL -> { val queryId = array[1].jsonPrimitive.content val filters = @@ -101,16 +106,22 @@ object CommandKSerializer : KSerializer { } CountCmd(queryId, filters) } + EventCmd.LABEL -> { EventCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject)) } + CloseCmd.LABEL -> { CloseCmd(array[1].jsonPrimitive.content) } + AuthCmd.LABEL -> { AuthCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject) as RelayAuthEvent) } - else -> throw IllegalArgumentException("Message $type is not supported") + + else -> { + throw IllegalArgumentException("Message $type is not supported") + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt index 57d2d56cab..843cf5b294 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt @@ -34,8 +34,6 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonObject diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt index 318af177ea..0ce754e0f2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt @@ -27,18 +27,15 @@ import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.int import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import kotlinx.serialization.json.longOrNull import kotlinx.serialization.json.put @@ -120,6 +117,7 @@ object FilterKSerializer : KSerializer { key.startsWith("#") -> { tags[key.substring(1)] = value.jsonArray.mapNotNull { it.jsonPrimitive.content } } + key.startsWith("&") -> { tagsAll[key.substring(1)] = value.jsonArray.mapNotNull { it.jsonPrimitive.content } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt index 086ed8951b..d5b79dbf47 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt @@ -70,19 +70,46 @@ class KotlinSerializationMapper { fun toJson(value: OptimizedSerializable): String = when (value) { - is Event -> json.encodeToString(EventKSerializer, value) - is Filter -> json.encodeToString(FilterKSerializer, value) - is Rumor -> json.encodeToString(RumorKSerializer, value) + is Event -> { + json.encodeToString(EventKSerializer, value) + } + + is Filter -> { + json.encodeToString(FilterKSerializer, value) + } + + is Rumor -> { + json.encodeToString(RumorKSerializer, value) + } + is EventTemplate<*> -> { @Suppress("UNCHECKED_CAST") json.encodeToString(EventTemplateKSerializer, value as EventTemplate) } - is Message -> json.encodeToString(MessageKSerializer, value) - is Command -> json.encodeToString(CommandKSerializer, value) - is BunkerRequest -> json.encodeToString(BunkerRequestKSerializer, value) - is BunkerResponse -> json.encodeToString(BunkerResponseKSerializer, value) - is BunkerMessage -> json.encodeToString(BunkerMessageKSerializer, value) - else -> throw IllegalArgumentException("Unsupported type: ${value::class}") + + is Message -> { + json.encodeToString(MessageKSerializer, value) + } + + is Command -> { + json.encodeToString(CommandKSerializer, value) + } + + is BunkerRequest -> { + json.encodeToString(BunkerRequestKSerializer, value) + } + + is BunkerResponse -> { + json.encodeToString(BunkerResponseKSerializer, value) + } + + is BunkerMessage -> { + json.encodeToString(BunkerMessageKSerializer, value) + } + + else -> { + throw IllegalArgumentException("Unsupported type: ${value::class}") + } } inline fun fromJsonTo(jsonStr: String): T { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt index f6e66da708..964b77d35a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt @@ -60,9 +60,11 @@ object MessageKSerializer : KSerializer { add(JsonPrimitive(value.subId)) add(EventKSerializer.serializeToElement(value.event)) } + is NoticeMessage -> { add(JsonPrimitive(value.message)) } + is OkMessage -> { add(JsonPrimitive(value.eventId)) // Jackson writes success as a string, not boolean @@ -71,16 +73,20 @@ object MessageKSerializer : KSerializer { add(JsonPrimitive(value.message)) } } + is AuthMessage -> { add(JsonPrimitive(value.challenge)) } + is NotifyMessage -> { add(JsonPrimitive(value.message)) } + is ClosedMessage -> { add(JsonPrimitive(value.subId)) add(JsonPrimitive(value.message)) } + is CountMessage -> { add(CountResultKSerializer.serializeToElement(value.result)) } @@ -100,12 +106,15 @@ object MessageKSerializer : KSerializer { val event = EventKSerializer.deserializeFromElement(array[2].jsonObject) EventMessage(subId, event) } + EoseMessage.LABEL -> { EoseMessage(array[1].jsonPrimitive.content) } + NoticeMessage.LABEL -> { NoticeMessage(array[1].jsonPrimitive.content) } + OkMessage.LABEL -> { OkMessage( eventId = array[1].jsonPrimitive.content, @@ -113,24 +122,31 @@ object MessageKSerializer : KSerializer { message = if (array.size > 3) array[3].jsonPrimitive.content else "", ) } + AuthMessage.LABEL -> { AuthMessage(array[1].jsonPrimitive.content) } + NotifyMessage.LABEL -> { NotifyMessage(array[1].jsonPrimitive.content) } + ClosedMessage.LABEL -> { ClosedMessage( subId = array[1].jsonPrimitive.content, message = if (array.size > 2) array[2].jsonPrimitive.content else "", ) } + CountMessage.LABEL -> { val queryId = array[1].jsonPrimitive.content val result = CountResultKSerializer.deserializeFromElement(array[2].jsonObject) CountMessage(queryId, result) } - else -> throw IllegalArgumentException("Message $type is not supported") + + else -> { + throw IllegalArgumentException("Message $type is not supported") + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt index 97502f068c..025e586cc4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt @@ -71,27 +71,55 @@ object BunkerMessageKSerializer : KSerializer { id: String, method: String, params: Array, - ): BunkerRequest { - return when (method) { - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.parse(id, params) - else -> BunkerRequest(id, method, params) + ): BunkerRequest = + when (method) { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign + .parse(id, params) + } + + else -> { + BunkerRequest(id, method, params) + } } - } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt index 1127255aae..9787b10eb3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt @@ -81,8 +81,14 @@ object BunkerResponseKSerializer : KSerializer { if (result != null) { when (result) { - BunkerResponseAck.RESULT -> return BunkerResponseAck.parse(id, result, error) - BunkerResponsePong.RESULT -> return BunkerResponsePong.parse(id, result, error) + BunkerResponseAck.RESULT -> { + return BunkerResponseAck.parse(id, result, error) + } + + BunkerResponsePong.RESULT -> { + return BunkerResponsePong.parse(id, result, error) + } + else -> { if (result.length == 64 && Hex.isHex(result)) { return BunkerResponsePublicKey.parse(id, result) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt index 8a105e1cff..811e46fe44 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt @@ -44,9 +44,7 @@ object Nip47NotificationKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Notification, - ) { - throw UnsupportedOperationException("NIP-47 Notification serialization not supported") - } + ): Unit = throw UnsupportedOperationException("NIP-47 Notification serialization not supported") override fun deserialize(decoder: Decoder): Notification { val jsonDecoder = decoder as JsonDecoder @@ -63,6 +61,7 @@ object Nip47NotificationKSerializer : KSerializer { ), ) } + NwcNotificationType.PAYMENT_SENT -> { PaymentSentNotification( notification = @@ -71,6 +70,7 @@ object Nip47NotificationKSerializer : KSerializer { ), ) } + NwcNotificationType.HOLD_INVOICE_ACCEPTED -> { val notifObj = jsonObject["notification"]?.jsonObject HoldInvoiceAcceptedNotification( @@ -88,7 +88,10 @@ object Nip47NotificationKSerializer : KSerializer { }, ) } - else -> throw IllegalArgumentException("Unknown notification type: $notificationType") + + else -> { + throw IllegalArgumentException("Unknown notification type: $notificationType") + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt index 0996ce75ac..691627409c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt @@ -68,9 +68,7 @@ object Nip47RequestKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Request, - ) { - throw UnsupportedOperationException("NIP-47 Request serialization not supported") - } + ): Unit = throw UnsupportedOperationException("NIP-47 Request serialization not supported") override fun deserialize(decoder: Decoder): Request { val jsonDecoder = decoder as JsonDecoder diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt index da7dfadc77..7c124de3cd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt @@ -60,9 +60,7 @@ object Nip47ResponseKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Response, - ) { - throw UnsupportedOperationException("NIP-47 Response serialization not supported") - } + ): Unit = throw UnsupportedOperationException("NIP-47 Response serialization not supported") override fun deserialize(decoder: Decoder): Response { val jsonDecoder = decoder as JsonDecoder @@ -73,7 +71,10 @@ object Nip47ResponseKSerializer : KSerializer { if (hasError) { return when (resultType) { - NwcMethod.PAY_INVOICE -> parsePayInvoiceError(jsonObject) + NwcMethod.PAY_INVOICE -> { + parsePayInvoiceError(jsonObject) + } + else -> { val error = jsonObject["error"]?.jsonObject?.let { parseNwcError(it) } NwcErrorResponse(resultType ?: "", error) @@ -83,19 +84,58 @@ object Nip47ResponseKSerializer : KSerializer { if (hasResult || resultType != null) { return when (resultType) { - NwcMethod.PAY_INVOICE -> parsePayInvoiceSuccess(jsonObject) - NwcMethod.PAY_KEYSEND -> parsePayKeysendSuccess(jsonObject) - NwcMethod.MAKE_INVOICE -> MakeInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) - NwcMethod.LOOKUP_INVOICE -> LookupInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) - NwcMethod.LIST_TRANSACTIONS -> parseListTransactionsSuccess(jsonObject) - NwcMethod.GET_BALANCE -> parseGetBalanceSuccess(jsonObject) - NwcMethod.GET_INFO -> parseGetInfoSuccess(jsonObject) - NwcMethod.GET_BUDGET -> parseGetBudgetSuccess(jsonObject) - NwcMethod.SIGN_MESSAGE -> parseSignMessageSuccess(jsonObject) - NwcMethod.CREATE_CONNECTION -> parseCreateConnectionSuccess(jsonObject) - NwcMethod.MAKE_HOLD_INVOICE -> MakeHoldInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) - NwcMethod.CANCEL_HOLD_INVOICE -> CancelHoldInvoiceSuccessResponse() - NwcMethod.SETTLE_HOLD_INVOICE -> SettleHoldInvoiceSuccessResponse() + NwcMethod.PAY_INVOICE -> { + parsePayInvoiceSuccess(jsonObject) + } + + NwcMethod.PAY_KEYSEND -> { + parsePayKeysendSuccess(jsonObject) + } + + NwcMethod.MAKE_INVOICE -> { + MakeInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + } + + NwcMethod.LOOKUP_INVOICE -> { + LookupInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + } + + NwcMethod.LIST_TRANSACTIONS -> { + parseListTransactionsSuccess(jsonObject) + } + + NwcMethod.GET_BALANCE -> { + parseGetBalanceSuccess(jsonObject) + } + + NwcMethod.GET_INFO -> { + parseGetInfoSuccess(jsonObject) + } + + NwcMethod.GET_BUDGET -> { + parseGetBudgetSuccess(jsonObject) + } + + NwcMethod.SIGN_MESSAGE -> { + parseSignMessageSuccess(jsonObject) + } + + NwcMethod.CREATE_CONNECTION -> { + parseCreateConnectionSuccess(jsonObject) + } + + NwcMethod.MAKE_HOLD_INVOICE -> { + MakeHoldInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + } + + NwcMethod.CANCEL_HOLD_INVOICE -> { + CancelHoldInvoiceSuccessResponse() + } + + NwcMethod.SETTLE_HOLD_INVOICE -> { + SettleHoldInvoiceSuccessResponse() + } + else -> { // backward compatibility: guess by result content val resultObj = jsonObject["result"]?.jsonObject @@ -111,13 +151,14 @@ object Nip47ResponseKSerializer : KSerializer { } private fun parseNwcError(obj: JsonObject): NwcError { - val code = obj["code"]?.jsonPrimitive?.content?.let { codeName -> - try { - NwcErrorCode.valueOf(codeName) - } catch (_: Exception) { - null + val code = + obj["code"]?.jsonPrimitive?.content?.let { codeName -> + try { + NwcErrorCode.valueOf(codeName) + } catch (_: Exception) { + null + } } - } return NwcError(code, obj["message"]?.jsonPrimitive?.content) } @@ -157,13 +198,14 @@ object Nip47ResponseKSerializer : KSerializer { return PayInvoiceErrorResponse( error?.let { PayInvoiceErrorResponse.PayInvoiceErrorParams( - code = it["code"]?.jsonPrimitive?.content?.let { codeName -> - try { - NwcErrorCode.valueOf(codeName) - } catch (_: Exception) { - null - } - }, + code = + it["code"]?.jsonPrimitive?.content?.let { codeName -> + try { + NwcErrorCode.valueOf(codeName) + } catch (_: Exception) { + null + } + }, message = it["message"]?.jsonPrimitive?.content, ) }, @@ -187,7 +229,7 @@ object Nip47ResponseKSerializer : KSerializer { return ListTransactionsSuccessResponse( result?.let { ListTransactionsSuccessResponse.ListTransactionsResult( - transactions = it["transactions"]?.jsonArray?.map { t -> parseTransaction(t.jsonObject) }, + transactions = it["transactions"]?.jsonArray?.mapNotNull { t -> parseTransaction(t.jsonObject) }, total_count = it["total_count"]?.jsonPrimitive?.longOrNull, ) }, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt index a8f4d5956e..f6fb5622a7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt @@ -20,10 +20,9 @@ */ package com.vitorpamplona.quartz.nip59Giftwrap.rumors.kotlinSerialization -import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.TagArrayKSerializer - import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.TagArrayKSerializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor From cd9d345a0e60cac3b1dec1ed39083f59f7643731 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 15 Mar 2026 21:34:45 +0100 Subject: [PATCH 112/172] spotlessApply --- .../kotlinSerialization/CommandKSerializer.kt | 13 ++- .../kotlinSerialization/EventKSerializer.kt | 4 +- .../kotlinSerialization/FilterKSerializer.kt | 4 +- .../KotlinSerializationMapper.kt | 45 ++++++-- .../kotlinSerialization/MessageKSerializer.kt | 18 +++- .../BunkerMessageKSerializer.kt | 72 +++++++++---- .../BunkerResponseKSerializer.kt | 10 +- .../Nip47NotificationKSerializer.kt | 11 +- .../Nip47RequestKSerializer.kt | 4 +- .../Nip47ResponseKSerializer.kt | 102 ++++++++++++------ .../kotlinSerialization/RumorKSerializer.kt | 3 +- 11 files changed, 206 insertions(+), 80 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt index 790a710ddf..4d3eba2998 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/CommandKSerializer.kt @@ -59,15 +59,19 @@ object CommandKSerializer : KSerializer { add(FilterKSerializer.serializeToElement(filter)) } } + is EventCmd -> { add(EventKSerializer.serializeToElement(value.event)) } + is CloseCmd -> { add(JsonPrimitive(value.subId)) } + is AuthCmd -> { add(EventKSerializer.serializeToElement(value.event)) } + is CountCmd -> { add(JsonPrimitive(value.queryId)) for (filter in value.filters) { @@ -93,6 +97,7 @@ object CommandKSerializer : KSerializer { } ReqCmd(subId, filters) } + CountCmd.LABEL -> { val queryId = array[1].jsonPrimitive.content val filters = @@ -101,16 +106,22 @@ object CommandKSerializer : KSerializer { } CountCmd(queryId, filters) } + EventCmd.LABEL -> { EventCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject)) } + CloseCmd.LABEL -> { CloseCmd(array[1].jsonPrimitive.content) } + AuthCmd.LABEL -> { AuthCmd(EventKSerializer.deserializeFromElement(array[1].jsonObject) as RelayAuthEvent) } - else -> throw IllegalArgumentException("Message $type is not supported") + + else -> { + throw IllegalArgumentException("Message $type is not supported") + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt index 57d2d56cab..4eba5b3973 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/EventKSerializer.kt @@ -34,8 +34,6 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonObject @@ -87,7 +85,7 @@ object EventKSerializer : KSerializer { var createdAt: Long = 0 var kind: Kind = 0 var tags: TagArray = emptyTagArray - var content: String = "" + var content = "" var sig: HexKey = "" for ((key, value) in jsonObject) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt index 318af177ea..0ce754e0f2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/FilterKSerializer.kt @@ -27,18 +27,15 @@ import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.int import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive -import kotlinx.serialization.json.long import kotlinx.serialization.json.longOrNull import kotlinx.serialization.json.put @@ -120,6 +117,7 @@ object FilterKSerializer : KSerializer { key.startsWith("#") -> { tags[key.substring(1)] = value.jsonArray.mapNotNull { it.jsonPrimitive.content } } + key.startsWith("&") -> { tagsAll[key.substring(1)] = value.jsonArray.mapNotNull { it.jsonPrimitive.content } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt index 086ed8951b..d5b79dbf47 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt @@ -70,19 +70,46 @@ class KotlinSerializationMapper { fun toJson(value: OptimizedSerializable): String = when (value) { - is Event -> json.encodeToString(EventKSerializer, value) - is Filter -> json.encodeToString(FilterKSerializer, value) - is Rumor -> json.encodeToString(RumorKSerializer, value) + is Event -> { + json.encodeToString(EventKSerializer, value) + } + + is Filter -> { + json.encodeToString(FilterKSerializer, value) + } + + is Rumor -> { + json.encodeToString(RumorKSerializer, value) + } + is EventTemplate<*> -> { @Suppress("UNCHECKED_CAST") json.encodeToString(EventTemplateKSerializer, value as EventTemplate) } - is Message -> json.encodeToString(MessageKSerializer, value) - is Command -> json.encodeToString(CommandKSerializer, value) - is BunkerRequest -> json.encodeToString(BunkerRequestKSerializer, value) - is BunkerResponse -> json.encodeToString(BunkerResponseKSerializer, value) - is BunkerMessage -> json.encodeToString(BunkerMessageKSerializer, value) - else -> throw IllegalArgumentException("Unsupported type: ${value::class}") + + is Message -> { + json.encodeToString(MessageKSerializer, value) + } + + is Command -> { + json.encodeToString(CommandKSerializer, value) + } + + is BunkerRequest -> { + json.encodeToString(BunkerRequestKSerializer, value) + } + + is BunkerResponse -> { + json.encodeToString(BunkerResponseKSerializer, value) + } + + is BunkerMessage -> { + json.encodeToString(BunkerMessageKSerializer, value) + } + + else -> { + throw IllegalArgumentException("Unsupported type: ${value::class}") + } } inline fun fromJsonTo(jsonStr: String): T { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt index f6e66da708..964b77d35a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt @@ -60,9 +60,11 @@ object MessageKSerializer : KSerializer { add(JsonPrimitive(value.subId)) add(EventKSerializer.serializeToElement(value.event)) } + is NoticeMessage -> { add(JsonPrimitive(value.message)) } + is OkMessage -> { add(JsonPrimitive(value.eventId)) // Jackson writes success as a string, not boolean @@ -71,16 +73,20 @@ object MessageKSerializer : KSerializer { add(JsonPrimitive(value.message)) } } + is AuthMessage -> { add(JsonPrimitive(value.challenge)) } + is NotifyMessage -> { add(JsonPrimitive(value.message)) } + is ClosedMessage -> { add(JsonPrimitive(value.subId)) add(JsonPrimitive(value.message)) } + is CountMessage -> { add(CountResultKSerializer.serializeToElement(value.result)) } @@ -100,12 +106,15 @@ object MessageKSerializer : KSerializer { val event = EventKSerializer.deserializeFromElement(array[2].jsonObject) EventMessage(subId, event) } + EoseMessage.LABEL -> { EoseMessage(array[1].jsonPrimitive.content) } + NoticeMessage.LABEL -> { NoticeMessage(array[1].jsonPrimitive.content) } + OkMessage.LABEL -> { OkMessage( eventId = array[1].jsonPrimitive.content, @@ -113,24 +122,31 @@ object MessageKSerializer : KSerializer { message = if (array.size > 3) array[3].jsonPrimitive.content else "", ) } + AuthMessage.LABEL -> { AuthMessage(array[1].jsonPrimitive.content) } + NotifyMessage.LABEL -> { NotifyMessage(array[1].jsonPrimitive.content) } + ClosedMessage.LABEL -> { ClosedMessage( subId = array[1].jsonPrimitive.content, message = if (array.size > 2) array[2].jsonPrimitive.content else "", ) } + CountMessage.LABEL -> { val queryId = array[1].jsonPrimitive.content val result = CountResultKSerializer.deserializeFromElement(array[2].jsonObject) CountMessage(queryId, result) } - else -> throw IllegalArgumentException("Message $type is not supported") + + else -> { + throw IllegalArgumentException("Message $type is not supported") + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt index 97502f068c..025e586cc4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerMessageKSerializer.kt @@ -71,27 +71,55 @@ object BunkerMessageKSerializer : KSerializer { id: String, method: String, params: Array, - ): BunkerRequest { - return when (method) { - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.parse(id, params) - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.METHOD_NAME -> - com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.parse(id, params) - else -> BunkerRequest(id, method, params) + ): BunkerRequest = + when (method) { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing + .parse(id, params) + } + + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign.METHOD_NAME -> { + com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign + .parse(id, params) + } + + else -> { + BunkerRequest(id, method, params) + } } - } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt index 1127255aae..9787b10eb3 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/kotlinSerialization/BunkerResponseKSerializer.kt @@ -81,8 +81,14 @@ object BunkerResponseKSerializer : KSerializer { if (result != null) { when (result) { - BunkerResponseAck.RESULT -> return BunkerResponseAck.parse(id, result, error) - BunkerResponsePong.RESULT -> return BunkerResponsePong.parse(id, result, error) + BunkerResponseAck.RESULT -> { + return BunkerResponseAck.parse(id, result, error) + } + + BunkerResponsePong.RESULT -> { + return BunkerResponsePong.parse(id, result, error) + } + else -> { if (result.length == 64 && Hex.isHex(result)) { return BunkerResponsePublicKey.parse(id, result) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt index 8a105e1cff..811e46fe44 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt @@ -44,9 +44,7 @@ object Nip47NotificationKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Notification, - ) { - throw UnsupportedOperationException("NIP-47 Notification serialization not supported") - } + ): Unit = throw UnsupportedOperationException("NIP-47 Notification serialization not supported") override fun deserialize(decoder: Decoder): Notification { val jsonDecoder = decoder as JsonDecoder @@ -63,6 +61,7 @@ object Nip47NotificationKSerializer : KSerializer { ), ) } + NwcNotificationType.PAYMENT_SENT -> { PaymentSentNotification( notification = @@ -71,6 +70,7 @@ object Nip47NotificationKSerializer : KSerializer { ), ) } + NwcNotificationType.HOLD_INVOICE_ACCEPTED -> { val notifObj = jsonObject["notification"]?.jsonObject HoldInvoiceAcceptedNotification( @@ -88,7 +88,10 @@ object Nip47NotificationKSerializer : KSerializer { }, ) } - else -> throw IllegalArgumentException("Unknown notification type: $notificationType") + + else -> { + throw IllegalArgumentException("Unknown notification type: $notificationType") + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt index 0996ce75ac..691627409c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt @@ -68,9 +68,7 @@ object Nip47RequestKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Request, - ) { - throw UnsupportedOperationException("NIP-47 Request serialization not supported") - } + ): Unit = throw UnsupportedOperationException("NIP-47 Request serialization not supported") override fun deserialize(decoder: Decoder): Request { val jsonDecoder = decoder as JsonDecoder diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt index da7dfadc77..78e4c435f4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt @@ -60,9 +60,7 @@ object Nip47ResponseKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Response, - ) { - throw UnsupportedOperationException("NIP-47 Response serialization not supported") - } + ): Unit = throw UnsupportedOperationException("NIP-47 Response serialization not supported") override fun deserialize(decoder: Decoder): Response { val jsonDecoder = decoder as JsonDecoder @@ -73,7 +71,10 @@ object Nip47ResponseKSerializer : KSerializer { if (hasError) { return when (resultType) { - NwcMethod.PAY_INVOICE -> parsePayInvoiceError(jsonObject) + NwcMethod.PAY_INVOICE -> { + parsePayInvoiceError(jsonObject) + } + else -> { val error = jsonObject["error"]?.jsonObject?.let { parseNwcError(it) } NwcErrorResponse(resultType ?: "", error) @@ -83,19 +84,58 @@ object Nip47ResponseKSerializer : KSerializer { if (hasResult || resultType != null) { return when (resultType) { - NwcMethod.PAY_INVOICE -> parsePayInvoiceSuccess(jsonObject) - NwcMethod.PAY_KEYSEND -> parsePayKeysendSuccess(jsonObject) - NwcMethod.MAKE_INVOICE -> MakeInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) - NwcMethod.LOOKUP_INVOICE -> LookupInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) - NwcMethod.LIST_TRANSACTIONS -> parseListTransactionsSuccess(jsonObject) - NwcMethod.GET_BALANCE -> parseGetBalanceSuccess(jsonObject) - NwcMethod.GET_INFO -> parseGetInfoSuccess(jsonObject) - NwcMethod.GET_BUDGET -> parseGetBudgetSuccess(jsonObject) - NwcMethod.SIGN_MESSAGE -> parseSignMessageSuccess(jsonObject) - NwcMethod.CREATE_CONNECTION -> parseCreateConnectionSuccess(jsonObject) - NwcMethod.MAKE_HOLD_INVOICE -> MakeHoldInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) - NwcMethod.CANCEL_HOLD_INVOICE -> CancelHoldInvoiceSuccessResponse() - NwcMethod.SETTLE_HOLD_INVOICE -> SettleHoldInvoiceSuccessResponse() + NwcMethod.PAY_INVOICE -> { + parsePayInvoiceSuccess(jsonObject) + } + + NwcMethod.PAY_KEYSEND -> { + parsePayKeysendSuccess(jsonObject) + } + + NwcMethod.MAKE_INVOICE -> { + MakeInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + } + + NwcMethod.LOOKUP_INVOICE -> { + LookupInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + } + + NwcMethod.LIST_TRANSACTIONS -> { + parseListTransactionsSuccess(jsonObject) + } + + NwcMethod.GET_BALANCE -> { + parseGetBalanceSuccess(jsonObject) + } + + NwcMethod.GET_INFO -> { + parseGetInfoSuccess(jsonObject) + } + + NwcMethod.GET_BUDGET -> { + parseGetBudgetSuccess(jsonObject) + } + + NwcMethod.SIGN_MESSAGE -> { + parseSignMessageSuccess(jsonObject) + } + + NwcMethod.CREATE_CONNECTION -> { + parseCreateConnectionSuccess(jsonObject) + } + + NwcMethod.MAKE_HOLD_INVOICE -> { + MakeHoldInvoiceSuccessResponse(parseTransaction(jsonObject["result"]?.jsonObject)) + } + + NwcMethod.CANCEL_HOLD_INVOICE -> { + CancelHoldInvoiceSuccessResponse() + } + + NwcMethod.SETTLE_HOLD_INVOICE -> { + SettleHoldInvoiceSuccessResponse() + } + else -> { // backward compatibility: guess by result content val resultObj = jsonObject["result"]?.jsonObject @@ -111,13 +151,14 @@ object Nip47ResponseKSerializer : KSerializer { } private fun parseNwcError(obj: JsonObject): NwcError { - val code = obj["code"]?.jsonPrimitive?.content?.let { codeName -> - try { - NwcErrorCode.valueOf(codeName) - } catch (_: Exception) { - null + val code = + obj["code"]?.jsonPrimitive?.content?.let { codeName -> + try { + NwcErrorCode.valueOf(codeName) + } catch (_: Exception) { + null + } } - } return NwcError(code, obj["message"]?.jsonPrimitive?.content) } @@ -157,13 +198,14 @@ object Nip47ResponseKSerializer : KSerializer { return PayInvoiceErrorResponse( error?.let { PayInvoiceErrorResponse.PayInvoiceErrorParams( - code = it["code"]?.jsonPrimitive?.content?.let { codeName -> - try { - NwcErrorCode.valueOf(codeName) - } catch (_: Exception) { - null - } - }, + code = + it["code"]?.jsonPrimitive?.content?.let { codeName -> + try { + NwcErrorCode.valueOf(codeName) + } catch (_: Exception) { + null + } + }, message = it["message"]?.jsonPrimitive?.content, ) }, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt index a8f4d5956e..f6fb5622a7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/rumors/kotlinSerialization/RumorKSerializer.kt @@ -20,10 +20,9 @@ */ package com.vitorpamplona.quartz.nip59Giftwrap.rumors.kotlinSerialization -import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.TagArrayKSerializer - import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.kotlinSerialization.TagArrayKSerializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.SerialDescriptor From 3cfa9e16194e5ca820c3ea7e931ecf5668c7324e Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 15 Mar 2026 21:36:53 +0100 Subject: [PATCH 113/172] compilation error fix: Changed map to mapNotNull --- .../kotlinSerialization/Nip47ResponseKSerializer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt index 78e4c435f4..7c124de3cd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt @@ -229,7 +229,7 @@ object Nip47ResponseKSerializer : KSerializer { return ListTransactionsSuccessResponse( result?.let { ListTransactionsSuccessResponse.ListTransactionsResult( - transactions = it["transactions"]?.jsonArray?.map { t -> parseTransaction(t.jsonObject) }, + transactions = it["transactions"]?.jsonArray?.mapNotNull { t -> parseTransaction(t.jsonObject) }, total_count = it["total_count"]?.jsonPrimitive?.longOrNull, ) }, From 0c939bd44702a100063a05f8bf41015f798e6695 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 16:50:34 -0400 Subject: [PATCH 114/172] converted from Java arraylist to kotlin arraylist --- .../nip01Core/kotlinSerialization/TagArrayKSerializer.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt index 3e6562808d..c9ebbe5eaf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/TagArrayKSerializer.kt @@ -79,11 +79,9 @@ object TagArrayKSerializer : KSerializer { innerList.add(s.jsonPrimitive.content) } } - @Suppress("UNCHECKED_CAST") - outerList.add(innerList.toArray(arrayOfNulls(innerList.size)) as Array) + outerList.add(innerList.toTypedArray()) } - @Suppress("UNCHECKED_CAST") - return outerList.toArray(arrayOfNulls>(outerList.size)) as TagArray + return outerList.toTypedArray() } fun serializeToElement(value: TagArray): JsonArray = From 6216f7a6180f87da6b6557da7a62c98e3347e17d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 16:55:00 -0400 Subject: [PATCH 115/172] no casts needed --- .../kotlinSerialization/KotlinSerializationMapperTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt index 2520242a04..95c2c9244c 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt @@ -487,7 +487,7 @@ class KotlinSerializationMapperTest { val json = """["EOSE","sub123"]""" val deserialized = KotlinSerializationMapper.fromJsonToMessage(json) assertTrue(deserialized is EoseMessage) - assertEquals("sub123", (deserialized as EoseMessage).subId) + assertEquals("sub123", (deserialized).subId) } @Test @@ -573,12 +573,12 @@ class KotlinSerializationMapperTest { val jacksonJson = JacksonMapper.toJson(cmd) val kotlinDeserialized = KotlinSerializationMapper.fromJsonToCommand(jacksonJson) assertTrue(kotlinDeserialized is CloseCmd) - assertEquals("sub1", (kotlinDeserialized as CloseCmd).subId) + assertEquals("sub1", (kotlinDeserialized).subId) val kotlinJson = KotlinSerializationMapper.toJson(cmd) val jacksonDeserialized = JacksonMapper.fromJsonToCommand(kotlinJson) assertTrue(jacksonDeserialized is CloseCmd) - assertEquals("sub1", (jacksonDeserialized as CloseCmd).subId) + assertEquals("sub1", (jacksonDeserialized).subId) } // ========================================================================= From 76c6f50fa811b46a71ba2bc018f6acc68d6efd0c Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Sun, 15 Mar 2026 20:57:02 +0000 Subject: [PATCH 116/172] Rename build step for Benchmark APK in workflow --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 61e76d85f9..8387a5298d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -54,7 +54,7 @@ jobs: name: FDroid Debug APK path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-debug.apk - - name: Build APK (gradle) + - name: Build Benchmark APK (gradle) run: ./gradlew assembleBenchmark - name: Upload Play APK Benchmark From cef8f5acdecf50d1463d1be32d63af10ea8254ad Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 17:14:57 -0400 Subject: [PATCH 117/172] Fixes test case --- .../kotlinSerialization/KotlinSerializationMapperTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt index 95c2c9244c..959b4de530 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapperTest.kt @@ -603,7 +603,7 @@ class KotlinSerializationMapperTest { @Test fun crossDeserializationBunkerRequest() { - val req = BunkerRequest("id1", "sign_event", arrayOf("eventJson")) + val req = BunkerRequest("id1", "sign_event", arrayOf("{\"created_at\":1234,\"kind\":1,\"tags\":[],\"content\":\"This is an unsigned event.\"}")) val jacksonJson = JacksonMapper.toJson(req) val kotlinDeserialized = KotlinSerializationMapper.fromJsonTo(jacksonJson) assertEquals(req.id, kotlinDeserialized.id) From 2fd29ab365efe54cb2d8fa60d15cb3729f1f85fa Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 17:15:16 -0400 Subject: [PATCH 118/172] uses quiet mode to avoid lots of warnings in the commit screen --- .git-hooks/pre-push | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.git-hooks/pre-push b/.git-hooks/pre-push index 903d42dc60..2c0dbdc27f 100755 --- a/.git-hooks/pre-push +++ b/.git-hooks/pre-push @@ -12,7 +12,7 @@ echo "$JAVA_HOME" echo "$(java -version)" echo "Running test... " -./gradlew test +./gradlew test --quiet status=$? From d996911cda68dc8682fadc1f52972dab3fce2765 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 15 Mar 2026 21:34:18 +0000 Subject: [PATCH 119/172] feat: add cross-platform CI for Linux, macOS, and Windows - Split build.yml into lint, test (matrix: 3 OS), build-android, and build-desktop (matrix: 3 OS with native packaging) jobs - Tests now run on ubuntu, macos, and windows to validate cross-platform compatibility - Desktop distributions (DEB, DMG, MSI) built on their native OS and uploaded as artifacts - Split create-release.yml into create-release, deploy-android, and deploy-desktop jobs so desktop distributions are included in releases - Android APK/AAB builds and Quartz publishing remain on Linux https://claude.ai/code/session_016dxFUErY4P6WRir4xxhCEr --- .github/workflows/build.yml | 130 ++++++++++++++++++++++++--- .github/workflows/create-release.yml | 128 +++++++++++++++++++------- 2 files changed, 215 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8387a5298d..9bb6fd2951 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Test/Build Android +name: Test/Build on: pull_request: @@ -7,10 +7,9 @@ on: branches: [main] jobs: - build: + lint: runs-on: ubuntu-latest - timeout-minutes: 30 - + timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v4 @@ -24,20 +23,84 @@ jobs: - name: Cache gradle uses: actions/cache@v4 with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- - name: Linter (gradle) run: ./gradlew spotlessCheck + test: + needs: lint + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + defaults: + run: + shell: bash + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 21 + + - name: Cache gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Test (gradle) run: ./gradlew test --no-daemon - name: Android Test Report uses: asadmansr/android-test-report-action@v1.2.0 - if: ${{ always() }} # IMPORTANT: run Android Test Report regardless + if: ${{ always() && matrix.os == 'ubuntu-latest' }} + + - name: Upload Test Results + uses: actions/upload-artifact@v4 + if: ${{ always() && matrix.os == 'ubuntu-latest' }} + with: + name: Test Reports + path: amethyst/build/reports + + build-android: + needs: test + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 21 + + - name: Cache gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- - name: Build APK (gradle) run: ./gradlew assembleDebug @@ -75,9 +138,54 @@ jobs: name: Compose Reports path: amethyst/build/compose_compiler - - name: Upload Test Results + build-desktop: + needs: test + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + task: packageDeb + artifact-name: Desktop Linux DEB + artifact-path: desktopApp/build/compose/binaries/main/deb/*.deb + - os: macos-latest + task: packageDmg + artifact-name: Desktop macOS DMG + artifact-path: desktopApp/build/compose/binaries/main/dmg/*.dmg + - os: windows-latest + task: packageMsi + artifact-name: Desktop Windows MSI + artifact-path: desktopApp/build/compose/binaries/main/msi/*.msi + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + defaults: + run: + shell: bash + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 21 + + - name: Cache gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build Desktop Distribution + run: ./gradlew :desktopApp:${{ matrix.task }} + + - name: Upload Desktop Distribution uses: actions/upload-artifact@v4 with: - name: Test Reports - path: amethyst/build/reports - + name: ${{ matrix.artifact-name }} + path: ${{ matrix.artifact-path }} diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 44f3d56fdc..70f337ecdf 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -6,7 +6,24 @@ on: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 jobs: - deploy: + create-release: + runs-on: ubuntu-latest + outputs: + upload_url: ${{ steps.create_release.outputs.upload_url }} + steps: + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: true + + deploy-android: + needs: create-release runs-on: ubuntu-latest steps: - name: Checkout code @@ -21,8 +38,10 @@ jobs: - name: Cache gradle uses: actions/cache@v4 with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- @@ -38,7 +57,6 @@ jobs: keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} env: - # override default build-tools version (29.0.3) -- optional BUILD_TOOLS_VERSION: "36.0.0" - name: Sign AAB (F-Droid) @@ -50,7 +68,6 @@ jobs: keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} env: - # override default build-tools version (29.0.3) -- optional BUILD_TOOLS_VERSION: "36.0.0" - name: Build APK @@ -65,7 +82,6 @@ jobs: keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} env: - # override default build-tools version (29.0.3) -- optional BUILD_TOOLS_VERSION: "36.0.0" - name: Sign APK (F-Droid) @@ -77,20 +93,8 @@ jobs: keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} env: - # override default build-tools version (29.0.3) -- optional BUILD_TOOLS_VERSION: "36.0.0" - - name: Create Release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: false - prerelease: true - # Google Play APK - name: Upload Play APK Universal Asset id: upload-release-asset-play-universal-apk @@ -98,7 +102,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -109,7 +113,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -120,7 +124,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -131,7 +135,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -142,7 +146,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -154,7 +158,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -165,7 +169,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -176,7 +180,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -187,7 +191,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk asset_content_type: application/zip @@ -198,13 +202,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk asset_content_type: application/zip - - # Google Play AAB - name: Upload Google Play AAB Asset id: upload-release-asset-play-aab @@ -212,7 +214,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab asset_name: amethyst-googleplay-${{ github.ref_name }}.aab asset_content_type: application/zip @@ -224,7 +226,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - upload_url: ${{ steps.create_release.outputs.upload_url }} + upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab asset_name: amethyst-fdroid-${{ github.ref_name }}.aab asset_content_type: application/zip @@ -236,3 +238,65 @@ jobs: ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + + deploy-desktop: + needs: create-release + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + task: packageDeb + format: deb + platform: linux + - os: macos-latest + task: packageDmg + format: dmg + platform: macos + - os: windows-latest + task: packageMsi + format: msi + platform: windows + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: 21 + + - name: Cache gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build Desktop Distribution + run: ./gradlew :desktopApp:${{ matrix.task }} + + - name: Find distribution file + id: find-dist + run: | + DIST_FILE=$(find desktopApp/build/compose/binaries/main/${{ matrix.format }} -type f \( -name "*.deb" -o -name "*.dmg" -o -name "*.msi" \) | head -1) + echo "path=$DIST_FILE" >> $GITHUB_OUTPUT + echo "name=$(basename $DIST_FILE)" >> $GITHUB_OUTPUT + + - name: Upload Desktop Distribution to Release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: ${{ steps.find-dist.outputs.path }} + asset_name: amethyst-desktop-${{ matrix.platform }}-${{ github.ref_name }}.${{ matrix.format }} + asset_content_type: application/octet-stream From 0e476c898839dd412f75f7ff4da068fbd2723d07 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 19:51:39 -0400 Subject: [PATCH 120/172] Adds test and some refinements. --- .../relays/common/BasicRelaySetupInfoModel.kt | 2 +- .../relays/common/RelayEventCountRow.kt | 4 +- .../relays/common/RelayEventCountViewModel.kt | 4 +- .../relays/dm/DMRelayListViewModel.kt | 3 +- .../indexer/IndexerRelayListViewModel.kt | 11 ++- .../nip37/PrivateOutboxRelayListViewModel.kt | 3 +- .../relays/nip65/Nip65RelayListViewModel.kt | 5 +- .../relays/proxy/ProxyRelayListViewModel.kt | 10 -- .../relays/search/SearchRelayListViewModel.kt | 3 +- amethyst/src/main/res/values/strings.xml | 4 + .../kotlinSerialization/MessageKSerializer.kt | 4 + .../relay/client/accessories/RelayLogger.kt | 2 + .../toClient/CountResultDeserializer.kt | 4 +- .../commands/toClient/MessageSerializer.kt | 4 +- .../relay/NostrClientQueryCountTest.kt | 93 +++++++++++++++++++ 15 files changed, 128 insertions(+), 28 deletions(-) create mode 100644 quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientQueryCountTest.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt index e198edcc7b..91bc1f373b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoModel.kt @@ -89,7 +89,7 @@ abstract class BasicRelaySetupInfoModel : ViewModel() { private fun loadCounts() { _countResults.value = emptyMap() - val client = Amethyst.instance.client + val client = account.client val relayList = _relays.value if (relayList.isEmpty()) return diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt index b62432f4b2..835eb764dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountRow.kt @@ -69,9 +69,9 @@ fun RelayEventCountRow( val countText = if (entry.approximate) { - "~${countToHumanReadable(entry.count, entry.label)}" + "~${countToHumanReadable(entry.count, stringRes(entry.label))}" } else { - countToHumanReadable(entry.count, entry.label) + countToHumanReadable(entry.count, stringRes(entry.label)) } Row( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt index 6f77913fff..5aaa39b6ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/RelayEventCountViewModel.kt @@ -29,13 +29,13 @@ data class RelayCountResult( ) { @Immutable data class CountEntry( - val label: String, + val label: Int, val count: Int, val approximate: Boolean = false, ) } data class CountFilter( - val label: String, + val label: Int, val filter: Filter, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt index a78df4519d..53a6a5ed01 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/dm/DMRelayListViewModel.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -39,7 +40,7 @@ class DMRelayListViewModel : BasicRelaySetupInfoModel() { override fun countFilters(relayUrl: NormalizedRelayUrl): List = listOf( CountFilter( - label = "events", + label = R.string.dms, filter = Filter( kinds = listOf(GiftWrapEvent.KIND, PrivateDmEvent.KIND), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt index c875c5a63f..46da1f83e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/indexer/IndexerRelayListViewModel.kt @@ -21,10 +21,13 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.indexer import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter +import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent @Stable class IndexerRelayListViewModel : BasicRelaySetupInfoModel() { @@ -39,12 +42,12 @@ class IndexerRelayListViewModel : BasicRelaySetupInfoModel() { override fun countFilters(relayUrl: NormalizedRelayUrl): List = listOf( CountFilter( - label = "kind 0", - filter = Filter(kinds = listOf(0)), + label = R.string.profiles, + filter = Filter(kinds = listOf(MetadataEvent.KIND)), ), CountFilter( - label = "kind 10002", - filter = Filter(kinds = listOf(10002)), + label = R.string.relay_settings_lower, + filter = Filter(kinds = listOf(AdvertisedRelayListEvent.KIND)), ), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt index 5cd3e96ff9..1003e95606 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip37/PrivateOutboxRelayListViewModel.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37 import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -39,7 +40,7 @@ class PrivateOutboxRelayListViewModel : BasicRelaySetupInfoModel() { override fun countFilters(relayUrl: NormalizedRelayUrl): List = listOf( CountFilter( - label = "events", + label = R.string.events, filter = Filter(authors = listOf(account.pubKey)), ), ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt index 0894a0f047..e6fb7883a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/nip65/Nip65RelayListViewModel.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.replace import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -136,7 +137,7 @@ class Nip65RelayListViewModel : ViewModel() { RelayCountResult( listOf( RelayCountResult.CountEntry( - label = "events", + label = R.string.events, count = result.count, approximate = result.approximate, ), @@ -155,7 +156,7 @@ class Nip65RelayListViewModel : ViewModel() { RelayCountResult( listOf( RelayCountResult.CountEntry( - label = "events", + label = R.string.events, count = result.count, approximate = result.approximate, ), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt index 01d07c40b6..e2a0f13596 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/proxy/ProxyRelayListViewModel.kt @@ -22,8 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.proxy import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter -import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @Stable @@ -35,12 +33,4 @@ class ProxyRelayListViewModel : BasicRelaySetupInfoModel() { override suspend fun saveRelayList(urlList: List) { account.saveProxyRelayList(urlList) } - - override fun countFilters(relayUrl: NormalizedRelayUrl): List = - listOf( - CountFilter( - label = "events", - filter = Filter(), - ), - ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt index aaf592a68b..2f219c5347 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/search/SearchRelayListViewModel.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.CountFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -39,7 +40,7 @@ class SearchRelayListViewModel : BasicRelaySetupInfoModel() { override fun countFilters(relayUrl: NormalizedRelayUrl): List = listOf( CountFilter( - label = "events", + label = R.string.events, filter = Filter(), ), ) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e322c05e7e..d2ae0d7247 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1802,4 +1802,8 @@ %1$d%% uptime Namecoin Settings Bitcoin Explorer (OTS) + events + DMs + profiles + relay settings diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt index 964b77d35a..34ee1b491a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/MessageKSerializer.kt @@ -90,6 +90,10 @@ object MessageKSerializer : KSerializer { is CountMessage -> { add(CountResultKSerializer.serializeToElement(value.result)) } + + is EoseMessage -> { + add(JsonPrimitive(value.subId)) + } } } jsonEncoder.encodeJsonElement(element) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt index eba39649b7..ceb4b36de0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/accessories/RelayLogger.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientLis import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.AuthMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.ClosedMessage +import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.CountMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EoseMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message @@ -62,6 +63,7 @@ class RelayLogger( is OkMessage -> if (debugReceiving) Log.d(logTag, "OK: ${msg.eventId} ${msg.success} ${msg.message}") is AuthMessage -> if (debugReceiving) Log.d(logTag, "Auth: ${msg.challenge}") is NotifyMessage -> if (debugReceiving) Log.d(logTag, "Notify: ${msg.message}") + is CountMessage -> if (debugReceiving) Log.d(logTag, "Count: ${msg.result.count} approx: ${msg.result.approximate}") is ClosedMessage -> Log.w(logTag, "Closed: ${msg.subId} ${msg.message}") } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/CountResultDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/CountResultDeserializer.kt index dd5cfa0bb0..3858f32368 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/CountResultDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/CountResultDeserializer.kt @@ -26,8 +26,8 @@ class CountResultDeserializer { companion object { fun fromJson(jsonObject: JsonNode): CountResult = CountResult( - count = jsonObject.get("count").asInt(), - approximate = jsonObject.get("approximate").asBoolean(), + count = jsonObject.get("count")?.asInt() ?: 0, + approximate = jsonObject.get("approximate")?.asBoolean() ?: false, ) } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/MessageSerializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/MessageSerializer.kt index 470360c800..345b9d9c46 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/MessageSerializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/relay/commands/toClient/MessageSerializer.kt @@ -72,8 +72,8 @@ class MessageSerializer : StdSerializer(Message::class.java) { countSerializer.serialize(msg.result, gen, provider) } - else -> { - null + is EoseMessage -> { + gen.writeString(msg.subId) } } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientQueryCountTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientQueryCountTest.kt new file mode 100644 index 0000000000..d58ed363fd --- /dev/null +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientQueryCountTest.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.relay + +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient +import com.vitorpamplona.quartz.nip01Core.relay.client.accessories.queryCountSuspend +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizeRelayUrl +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlin.test.Test + +class NostrClientQueryCountTest : BaseNostrClientTest() { + val fiatjaf = "wss://pyramid.fiatjaf.com".normalizeRelayUrl() + val utxo = "wss://news.utxo.one".normalizeRelayUrl() + + val metadata = Filter(kinds = listOf(0)) + val outboxRelays = Filter(kinds = listOf(10002)) + + @Test + fun testQueryCountSuspend() = + runBlocking { + val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + val client = NostrClient(socketBuilder, appScope) + + val result = client.queryCountSuspend(relay = fiatjaf, filter = metadata) + + assertTrue((result?.count ?: 0) > 1) + + client.disconnect() + appScope.cancel() + } + + @Test + fun testQueryCountSuspendAllEvents() = + runBlocking { + val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + val client = NostrClient(socketBuilder, appScope) + + val result = client.queryCountSuspend(relay = fiatjaf, filter = Filter()) + + assertTrue((result?.count ?: 0) > 1) + + client.disconnect() + appScope.cancel() + } + + @Test + fun testQueryCountSuspendMultipleRelays() = + runBlocking { + val appScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + val client = NostrClient(socketBuilder, appScope) + + val result = + client.queryCountSuspend( + filters = + mapOf( + fiatjaf to listOf(metadata, outboxRelays), + utxo to listOf(metadata, outboxRelays), + ), + ) + + result.forEach { url, result -> + println("${url.url}: ${result.count}") + assertTrue(result.count > 1) + } + + client.disconnect() + appScope.cancel() + } +} From 1cdd061b06006452b137ffcb3f522371b77c2ca5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 20:03:39 -0400 Subject: [PATCH 121/172] removing warnings --- .../quartz/nip47WalletConnect/ResponseTest.kt | 82 ++++++++----------- 1 file changed, 35 insertions(+), 47 deletions(-) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt index c26e05bf01..ff7ad12a79 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/ResponseTest.kt @@ -94,13 +94,13 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result) - assertEquals("incoming", response.result?.type) - assertEquals("lnbc50n1...", response.result?.invoice) - assertEquals("test", response.result?.description) - assertEquals("abc", response.result?.payment_hash) - assertEquals(5000L, response.result?.amount) - assertEquals(1693876497L, response.result?.created_at) - assertEquals(1694876497L, response.result?.expires_at) + assertEquals("incoming", response.result.type) + assertEquals("lnbc50n1...", response.result.invoice) + assertEquals("test", response.result.description) + assertEquals("abc", response.result.payment_hash) + assertEquals(5000L, response.result.amount) + assertEquals(1693876497L, response.result.created_at) + assertEquals(1694876497L, response.result.expires_at) } // --- LookupInvoice Success --- @@ -111,11 +111,11 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result) - assertEquals("incoming", response.result?.type) - assertEquals("settled", response.result?.state) - assertEquals("hash123", response.result?.payment_hash) - assertEquals(1000L, response.result?.amount) - assertEquals(1694876497L, response.result?.settled_at) + assertEquals("incoming", response.result.type) + assertEquals("settled", response.result.state) + assertEquals("hash123", response.result.payment_hash) + assertEquals(1000L, response.result.amount) + assertEquals(1694876497L, response.result.settled_at) } // --- ListTransactions Success --- @@ -127,34 +127,22 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result?.transactions) - assertEquals(2, response.result?.transactions?.size) + assertEquals(2, response.result.transactions.size) assertEquals( "incoming", - response.result - ?.transactions - ?.get(0) - ?.type, + response.result.transactions[0].type, ) assertEquals( 100L, - response.result - ?.transactions - ?.get(0) - ?.amount, + response.result.transactions[0].amount, ) assertEquals( "outgoing", - response.result - ?.transactions - ?.get(1) - ?.type, + response.result.transactions[1].type, ) assertEquals( 200L, - response.result - ?.transactions - ?.get(1) - ?.amount, + response.result.transactions[1].amount, ) } @@ -164,7 +152,7 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result?.transactions) - assertEquals(0, response.result?.transactions?.size) + assertEquals(0, response.result.transactions.size) } // --- GetBalance Success --- @@ -194,14 +182,14 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result) - assertEquals("MyNode", response.result?.alias) - assertEquals("#ff9900", response.result?.color) - assertEquals("abc123", response.result?.pubkey) - assertEquals("mainnet", response.result?.network) - assertEquals(800000L, response.result?.block_height) - assertEquals("hash", response.result?.block_hash) - assertEquals(listOf("pay_invoice", "get_balance"), response.result?.methods) - assertEquals(listOf("payment_received"), response.result?.notifications) + assertEquals("MyNode", response.result.alias) + assertEquals("#ff9900", response.result.color) + assertEquals("abc123", response.result.pubkey) + assertEquals("mainnet", response.result.network) + assertEquals(800000L, response.result.block_height) + assertEquals("hash", response.result.block_hash) + assertEquals(listOf("pay_invoice", "get_balance"), response.result.methods) + assertEquals(listOf("payment_received"), response.result.notifications) } // --- MakeHoldInvoice Success --- @@ -212,8 +200,8 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result) - assertEquals("lnbc...", response.result?.invoice) - assertEquals("hash", response.result?.payment_hash) + assertEquals("lnbc...", response.result.invoice) + assertEquals("hash", response.result.payment_hash) } // --- CancelHoldInvoice Success --- @@ -287,10 +275,10 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result) - assertEquals(50000L, response.result?.used_budget) - assertEquals(100000L, response.result?.total_budget) - assertEquals(1700000000L, response.result?.renews_at) - assertEquals("monthly", response.result?.renewal_period) + assertEquals(50000L, response.result.used_budget) + assertEquals(100000L, response.result.total_budget) + assertEquals(1700000000L, response.result.renews_at) + assertEquals("monthly", response.result.renewal_period) } @Test @@ -312,8 +300,8 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result) - assertEquals("Hello Nostr", response.result?.message) - assertEquals("sig123abc", response.result?.signature) + assertEquals("Hello Nostr", response.result.message) + assertEquals("sig123abc", response.result.signature) } // --- CreateConnection Success --- @@ -324,7 +312,7 @@ class ResponseTest { val response = OptimizedJsonMapper.fromJsonTo(json) assertIs(response) assertNotNull(response.result) - assertEquals("walletpub123", response.result?.wallet_pubkey) + assertEquals("walletpub123", response.result.wallet_pubkey) } // --- GetInfo with extended fields --- From 19ca7afcb8dd3ab65b50310d4df8a8d9db299226 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 20:06:20 -0400 Subject: [PATCH 122/172] fixes warnings --- .../quartz/nip47WalletConnect/AlbyInteropTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt index f6221cdc62..4b70331eb0 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/AlbyInteropTest.kt @@ -581,8 +581,8 @@ class AlbyInteropTest { assertIs(response) assertEquals(13, response.result?.methods?.size) assertEquals(3, response.result?.notifications?.size) - assertTrue(response.result?.methods?.contains(NwcMethod.GET_BUDGET) == true) - assertTrue(response.result?.methods?.contains(NwcMethod.SIGN_MESSAGE) == true) - assertTrue(response.result?.methods?.contains(NwcMethod.CREATE_CONNECTION) == true) + assertEquals(response.result?.methods?.contains(NwcMethod.GET_BUDGET), true) + assertEquals(response.result?.methods?.contains(NwcMethod.SIGN_MESSAGE), true) + assertEquals(response.result?.methods?.contains(NwcMethod.CREATE_CONNECTION), true) } } From 49f5d9518ef8cd2dbec2705116c3c7981a0647d8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 20:26:31 -0400 Subject: [PATCH 123/172] Fixes warnings --- .../quartz/nip49PrivKeyEnc/NIP49Test.kt | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt index 5dfcfb8ba1..3eeae80550 100644 --- a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt @@ -29,15 +29,15 @@ import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -public class NIP49Test { +class NIP49Test { companion object { - val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p" + const val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p" - val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683" - val TEST_CASE_PASSWORD = "nostr" + const val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683" + const val TEST_CASE_PASSWORD = "nostr" val MAIN_TEST_CASES = - listOf( + listOf( Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00), Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01), Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02), @@ -55,12 +55,9 @@ public class NIP49Test { @Test fun decodeBech32() { - val data = - Nip49.EncryptedInfo.decodePayload( - TEST_CASE, - )!! + val data = Nip49.EncryptedInfo.decodePayload(TEST_CASE) - assertEquals(2.toByte(), data.version) + assertEquals(2.toByte(), data!!.version) assertEquals(16.toByte(), data.logn) assertEquals("52d7c3f8580e7b41953381e5bc49646b", data.salt.toHexKey()) assertEquals("c33f02a7dcaac8bdd8da23cd449783240b6ebc12edeea7bf", data.nonce.toHexKey()) @@ -77,7 +74,7 @@ public class NIP49Test { @Test fun encryptDecryptTestCase() { val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, TEST_CASE_PASSWORD, 16, 0) - val decrypted = nip49.decrypt(encrypted!!, TEST_CASE_PASSWORD) + val decrypted = nip49.decrypt(encrypted, TEST_CASE_PASSWORD) assertEquals(TEST_CASE_EXPECTED, decrypted) } @@ -89,7 +86,7 @@ public class NIP49Test { assertNotNull(encrypted) - val decrypted = nip49.decrypt(encrypted!!, it.password) + val decrypted = nip49.decrypt(encrypted, it.password) assertEquals(it.secretKey, decrypted) } @@ -108,7 +105,7 @@ public class NIP49Test { assertNotNull(encrypted) - val decrypted = nip49.decrypt(encrypted!!, samePassword2) + val decrypted = nip49.decrypt(encrypted, samePassword2) assertEquals(TEST_CASE_EXPECTED, decrypted) } From e87ac7468d96e325caddb5667efe5b6a6de48c0e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 20:29:05 -0400 Subject: [PATCH 124/172] Removing Locale deprecations --- .../com/vitorpamplona/amethyst/model/Account.kt | 3 +-- .../amethyst/model/AccountSettings.kt | 7 +++---- .../amethyst/model/AccountSyncedSettings.kt | 9 ++++----- .../ui/screen/loggedIn/AccountViewModel.kt | 3 +-- .../screen/loggedIn/settings/UserSettingsScreen.kt | 10 +++++----- .../ui/components/TranslatableRichTextViewer.kt | 14 +++++++------- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index e7397ce9cf..ca19be854e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -226,7 +226,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.math.BigDecimal -import java.util.Locale import kotlin.coroutines.cancellation.CancellationException @OptIn(DelicateCoroutinesApi::class) @@ -508,7 +507,7 @@ class Account( sendNewAppSpecificData() } - suspend fun updateTranslateTo(languageCode: Locale) { + suspend fun updateTranslateTo(languageCode: String) { if (settings.updateTranslateTo(languageCode)) { sendNewAppSpecificData() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 5e6ad853d2..daa174de48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -63,7 +63,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.serialization.Serializable -import java.util.Locale val DefaultChannels = listOf( @@ -332,11 +331,11 @@ class AccountSettings( saveAccountSettings() } - fun translateToContains(languageCode: Locale) = + fun translateToContains(languageCode: String) = syncedSettings.languages.translateTo.value - .contains(languageCode.language) + .contains(languageCode) - fun updateTranslateTo(languageCode: Locale): Boolean { + fun updateTranslateTo(languageCode: String): Boolean { if (syncedSettings.languages.updateTranslateTo(languageCode)) { saveAccountSettings() return true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt index 3789e1c983..a1ab6665bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettings.kt @@ -27,7 +27,6 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import java.util.Locale @Stable class AccountSyncedSettings( @@ -165,11 +164,11 @@ class AccountLanguagePreferences( dontTranslateFrom.update { it - languageCode } } - fun translateToContains(languageCode: Locale) = translateTo.value.contains(languageCode.language) + fun translateToContains(languageCode: String) = translateTo.value.contains(languageCode) - fun updateTranslateTo(languageCode: Locale): Boolean { - if (translateTo.value != languageCode.language) { - translateTo.tryEmit(languageCode.language) + fun updateTranslateTo(languageCode: String): Boolean { + if (translateTo.value != languageCode) { + translateTo.tryEmit(languageCode) return true } return false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 09a901b7d8..312e6b541c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -158,7 +158,6 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.Locale @Stable class AccountViewModel( @@ -1009,7 +1008,7 @@ class AccountViewModel( fun removeDontTranslateFrom(languageCode: String) = launchSigner { account.removeDontTranslateFrom(languageCode) } - fun updateTranslateTo(languageCode: Locale) = launchSigner { account.updateTranslateTo(languageCode) } + fun updateTranslateTo(languageCode: String) = launchSigner { account.updateTranslateTo(languageCode) } fun prefer( source: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt index ef2f01e2ce..16af23f491 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/UserSettingsScreen.kt @@ -213,7 +213,7 @@ fun TranslateToSetting(accountViewModel: AccountViewModel) { verticalAlignment = Alignment.CenterVertically, ) { Text( - text = JavaLocale(currentTranslateTo).displayName, + text = JavaLocale.forLanguageTag(currentTranslateTo).displayName, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, ) @@ -227,7 +227,7 @@ fun TranslateToSetting(accountViewModel: AccountViewModel) { SearchableLanguageList( languages = allLanguages, onSelect = { locale -> - accountViewModel.updateTranslateTo(locale) + accountViewModel.updateTranslateTo(locale.language) showPicker = false }, ) @@ -265,7 +265,7 @@ fun DontTranslateFromSetting(accountViewModel: AccountViewModel) { InputChip( selected = true, onClick = { accountViewModel.removeDontTranslateFrom(languageCode) }, - label = { Text(JavaLocale(languageCode).displayName) }, + label = { Text(JavaLocale.forLanguageTag(languageCode).displayName) }, trailingIcon = { Icon( imageVector = Icons.Default.Close, @@ -367,8 +367,8 @@ private fun LanguagePreferenceCard( preference: String, accountViewModel: AccountViewModel, ) { - val sourceName = JavaLocale(source).displayName - val targetName = JavaLocale(target).displayName + val sourceName = JavaLocale.forLanguageTag(source).displayName + val targetName = JavaLocale.forLanguageTag(target).displayName OutlinedCard(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.padding(12.dp)) { diff --git a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index a48f700ac7..697583552c 100644 --- a/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/amethyst/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -178,9 +178,9 @@ private fun TranslationMessage( buildAnnotatedString { appendLink(stringRes(R.string.translations_auto), textColor) { langSettingsPopupExpanded = !langSettingsPopupExpanded } append(" ${stringRes(R.string.translations_translated_from)} ") - appendLink(Locale(source).displayName, textColor) { onChangeWhatToShow(true) } + appendLink(Locale.forLanguageTag(source).displayName, textColor) { onChangeWhatToShow(true) } append(" ${stringRes(R.string.translations_to)} ") - appendLink(Locale(target).displayName, textColor) { onChangeWhatToShow(false) } + appendLink(Locale.forLanguageTag(target).displayName, textColor) { onChangeWhatToShow(false) } }, style = LocalTextStyle.current.copy( @@ -213,7 +213,7 @@ private fun TranslationMessage( Text( stringRes( R.string.translations_never_translate_from_lang, - Locale(source).displayName, + Locale.forLanguageTag(source).displayName, ), ) } @@ -242,7 +242,7 @@ private fun TranslationMessage( Text( stringRes( R.string.translations_show_in_lang_first, - Locale(source).displayName, + Locale.forLanguageTag(source).displayName, ), ) } @@ -272,7 +272,7 @@ private fun TranslationMessage( Text( stringRes( R.string.translations_show_in_lang_first, - Locale(target).displayName, + Locale.forLanguageTag(target).displayName, ), ) } @@ -292,7 +292,7 @@ private fun TranslationMessage( DropdownMenuItem( text = { Row(verticalAlignment = Alignment.CenterVertically) { - if (accountViewModel.account.settings.translateToContains(lang)) { + if (accountViewModel.account.settings.translateToContains(lang.language)) { Icon( imageVector = Icons.Default.Check, contentDescription = null, @@ -314,7 +314,7 @@ private fun TranslationMessage( }, onClick = { langSettingsPopupExpanded = false - accountViewModel.updateTranslateTo(lang) + accountViewModel.updateTranslateTo(lang.language) }, ) } From bda7e48a841fb462939beda24739f17cde2d43ed Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 20:33:31 -0400 Subject: [PATCH 125/172] removes more warnings --- .../quartz/nip01Core/tags/aTag/TagArrayExt.kt | 2 +- .../quartz/nip64Chess/ChessEngine.jvmAndroid.kt | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/TagArrayExt.kt index d79c73717f..f3e9df4722 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/TagArrayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/TagArrayExt.kt @@ -39,7 +39,7 @@ fun TagArray.isTaggedAddressableKind(kindStr: String) = this.any(ATag::isTaggedW fun TagArray.getTagOfAddressableKind(kind: Int) = this.fastFirstNotNullOfOrNull(ATag::parseIfOfKind, kind.toString()) -fun TagArray.getTagOfAddressableKind(kindStr: String) = this.fastFirstNotNullOfOrNull(ATag::parseIfOfKind, kindStr.toString()) +fun TagArray.getTagOfAddressableKind(kindStr: String) = this.fastFirstNotNullOfOrNull(ATag::parseIfOfKind, kindStr) fun TagArray.taggedATags() = this.mapNotNull(ATag::parse) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt index 42f5dd8911..d9b192d9db 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt @@ -119,7 +119,6 @@ actual class ChessEngine { val sq = Square.fromValue(square.uppercase()) return board .legalMoves() - .filterNotNull() .filter { it.from == sq } .map { it.to.toString().lowercase() } } @@ -132,7 +131,7 @@ actual class ChessEngine { } else { false } - } catch (e: Exception) { + } catch (_: Exception) { false } @@ -156,7 +155,7 @@ actual class ChessEngine { val move = Move(fromSquare, toSquare, promotionPiece) board.legalMoves().contains(move) - } catch (e: Exception) { + } catch (_: Exception) { false } @@ -192,7 +191,7 @@ actual class ChessEngine { val toSquare = move.to val piece = board.getPiece(fromSquare) val pt = piece.pieceType ?: return move.toString() - val promotionPiece = move.promotion ?: Piece.NONE + val promotionPiece = move.promotion // Castling if (pt == com.github.bhlangonijr.chesslib.PieceType.KING) { @@ -213,7 +212,7 @@ actual class ChessEngine { board.getPiece(toSquare) != Piece.NONE || ( pt == com.github.bhlangonijr.chesslib.PieceType.PAWN && - epTarget != null && epTarget != Square.NONE && toSquare == epTarget + epTarget != Square.NONE && toSquare == epTarget ) if (pt != com.github.bhlangonijr.chesslib.PieceType.PAWN) { @@ -221,7 +220,7 @@ actual class ChessEngine { // Disambiguation: check if other pieces of same type can reach the same square val ambiguous = - board.legalMoves().filterNotNull().filter { + board.legalMoves().filter { it.to == toSquare && board.getPiece(it.from).pieceType == pt && it.from != fromSquare @@ -342,7 +341,7 @@ actual class ChessEngine { blackKingSide = board.castleRight.toString().contains("k"), blackQueenSide = board.castleRight.toString().contains("q"), ), - enPassantSquare = board.enPassantTarget?.let { it.toString().lowercase() }, + enPassantSquare = board.enPassantTarget.toString().lowercase(), halfMoveClock = board.halfMoveCounter, ) } From 8b4a45cb5b980d23e7b553be97b5d90da71bf246 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 20:52:01 -0400 Subject: [PATCH 126/172] Removing deprecated library addresses and deprecated apis --- commons/build.gradle.kts | 16 ++++++++-------- desktopApp/build.gradle.kts | 6 +++--- gradle/libs.versions.toml | 11 +++++++++++ quartz/build.gradle.kts | 25 ++++++++++++++----------- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 1d6ce95e1b..c55f9e4099 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -49,12 +49,12 @@ kotlin { implementation(project(":quartz")) // Compose Multiplatform - implementation(compose.ui) - implementation(compose.foundation) - implementation(compose.runtime) - implementation(compose.material3) - implementation(compose.materialIconsExtended) - implementation(compose.components.uiToolingPreview) + implementation(libs.jetbrains.compose.ui) + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.compose.material3) + implementation(libs.jetbrains.compose.material.icons.extended) + implementation(libs.jetbrains.compose.ui.tooling.preview) // Lifecycle ViewModel (KMP since 2.8.0) implementation(libs.androidx.lifecycle.viewmodel.compose) @@ -71,7 +71,7 @@ kotlin { api(libs.kotlinx.collections.immutable) // Compose Multiplatform Resources - implementation(compose.components.resources) + implementation(libs.jetbrains.compose.components.resources) } } @@ -94,7 +94,7 @@ kotlin { dependencies { // Desktop-specific Compose implementation(compose.desktop.currentOs) - implementation(compose.uiTooling) + implementation(libs.jetbrains.compose.ui.tooling) // Secure key storage via OS keychain (macOS/Windows/Linux) implementation(libs.java.keyring) diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 85c1c5607e..1a728e1716 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -23,9 +23,9 @@ kotlin { dependencies { implementation(compose.desktop.currentOs) - implementation(compose.material3) - implementation(compose.materialIconsExtended) - implementation(compose.components.resources) + implementation(libs.jetbrains.compose.material3) + implementation(libs.jetbrains.compose.material.icons.extended) + implementation(libs.jetbrains.compose.components.resources) // Quartz Nostr library (will use JVM target) implementation(project(":quartz")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 95a41277f4..998b19aa04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -39,6 +39,8 @@ lazysodiumJava = "5.2.0" lifecycleRuntimeKtx = "2.10.0" lightcompressor-enhanced = "1.6.0" markdown = "f92ef49c9d" +material3 = "1.9.0" +materialIconsExtended = "1.7.3" media3 = "1.9.2" mockk = "1.14.9" kotlinx-coroutines-test = "1.10.2" @@ -53,6 +55,7 @@ spotless = "8.3.0" tarsosdsp = "2.5" torAndroid = "0.4.9.5" translate = "17.0.3" +jetbrainsCompose = "1.10.2" unifiedpush = "3.0.10" vico-charts = "2.4.3" zelory = "3.0.1" @@ -122,6 +125,14 @@ dev-whyoleg-cryptography-provider-apple-optimal = { module = "dev.whyoleg.crypto drfonfon-geohash = { group = "com.github.drfonfon", name = "android-kotlin-geohash", version.ref = "androidKotlinGeohash" } firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" } firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } +jetbrains-compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "jetbrainsCompose" } +jetbrains-compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "jetbrainsCompose" } +jetbrains-compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "materialIconsExtended" } +jetbrains-compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" } +jetbrains-compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "jetbrainsCompose" } +jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "jetbrainsCompose" } +jetbrains-compose-ui-tooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "jetbrainsCompose" } +jetbrains-compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "jetbrainsCompose" } google-mlkit-language-id = { group = "com.google.mlkit", name = "language-id", version.ref = "languageId" } google-mlkit-translate = { group = "com.google.mlkit", name = "translate", version.ref = "translate" } jackson-module-kotlin = { group = "com.fasterxml.jackson.module", name = "jackson-module-kotlin", version.ref = "jacksonModuleKotlin" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 488f904f11..a2dc608f34 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -1,11 +1,13 @@ @file:OptIn(ExperimentalSpmForKmpFeature::class) import com.vanniktech.maven.publish.KotlinMultiplatform +import com.vanniktech.maven.publish.SourcesJar import io.github.frankois944.spmForKmp.swiftPackageConfig import io.github.frankois944.spmForKmp.utils.ExperimentalSpmForKmpFeature import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest + plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidKotlinMultiplatformLibrary) @@ -66,17 +68,18 @@ kotlin { val libsodiumDefFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") // This generates the Libsodium definition file, necessary for creating native bindings(a Kotlin API) for libsodium(for iOS). - val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { - outputs.file(libsodiumDefFile) - doLast { - if (!libsodiumDefFile.exists()) { - libsodiumDefFile.parentFile.mkdirs() - libsodiumDefFile.writeText("package = Clibsodium\n") - libsodiumDefFile.appendText("staticLibraries = libsodium.a libsodium-simulator.a\n") - libsodiumDefFile.appendText("libraryPaths = ${libsodiumPath.absolutePath}/ios/lib ${libsodiumPath.absolutePath}/ios-simulators/lib\n") + val libsodiumDefFileGeneration = + tasks.register("GenerateSodiumCinteropFile") { + outputs.file(libsodiumDefFile) + doLast { + if (!libsodiumDefFile.exists()) { + libsodiumDefFile.parentFile.mkdirs() + libsodiumDefFile.writeText("package = Clibsodium\n") + libsodiumDefFile.appendText("staticLibraries = libsodium.a libsodium-simulator.a\n") + libsodiumDefFile.appendText("libraryPaths = ${libsodiumPath.absolutePath}/ios/lib ${libsodiumPath.absolutePath}/ios-simulators/lib\n") + } } } - } listOf( iosArm64(), @@ -92,7 +95,7 @@ kotlin { headers( "$libsodiumHeaderFilesPath/crypto_aead_xchacha20poly1305.h", "$libsodiumHeaderFilesPath/crypto_core_hchacha20.h", - "$libsodiumHeaderFilesPath/crypto_stream_chacha20.h" + "$libsodiumHeaderFilesPath/crypto_stream_chacha20.h", ) } @@ -315,7 +318,7 @@ mavenPublishing { configure( KotlinMultiplatform( // whether to publish a sources jar - sourcesJar = true, + sourcesJar = SourcesJar.Sources(), ), ) From d10b43c098c2a1db6c09d832aec1b2008b21efe4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 20:55:55 -0400 Subject: [PATCH 127/172] Migrates old Preview annotation --- .../com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt | 2 +- .../com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt | 2 +- .../com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt | 2 +- .../com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt | 2 +- .../com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt | 2 +- .../com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt | 2 +- .../com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt | 2 +- .../com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Bookmark.kt | 2 +- .../com/vitorpamplona/amethyst/commons/icons/Following.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt | 2 +- .../kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt | 2 +- .../amethyst/commons/robohash/RobohashAssembler.kt | 2 +- .../amethyst/commons/robohash/parts/Accessory0Seven.kt | 2 +- .../amethyst/commons/robohash/parts/Accessory1Nose.kt | 2 +- .../amethyst/commons/robohash/parts/Accessory2HornRed.kt | 2 +- .../amethyst/commons/robohash/parts/Accessory3Button.kt | 2 +- .../amethyst/commons/robohash/parts/Accessory4Satellite.kt | 2 +- .../amethyst/commons/robohash/parts/Accessory5Mustache.kt | 2 +- .../amethyst/commons/robohash/parts/Body0Trooper.kt | 2 +- .../vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt | 2 +- .../amethyst/commons/robohash/parts/Body2Thinnest.kt | 2 +- .../vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt | 2 +- .../vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt | 2 +- .../vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt | 2 +- .../amethyst/commons/robohash/parts/Body6Ironman.kt | 2 +- .../amethyst/commons/robohash/parts/Body7Neckthinner.kt | 2 +- .../vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt | 2 +- .../vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt | 2 +- 44 files changed, 44 insertions(+), 44 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt index ea46a6db8d..e640624d3c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Amethyst.kt @@ -32,8 +32,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt index e4e0aa72d1..be8d13a795 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Btc.kt @@ -33,8 +33,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt index 1a831f0b38..e1de172b11 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Cashu.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt index 5daa578c59..53c4ec730a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Coffee.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt index d290cee854..9e0295efe1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Flowerstr.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt index 7ea0d4104d..2630d212e9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Footstr.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt index 7362b1ac21..d28818a2d2 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Gamestr.kt @@ -28,8 +28,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt index 41166aadb0..bec0a2ff69 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Grownostr.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt index 242adac53c..7b96119215 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Lightning.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt index 50b96eee90..8f8174d98b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Mate.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt index ee6b91f121..511507d62e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Nostr.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt index dab15b1f6e..f4f989545b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Plebs.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt index ccbd67b1e4..3c47a957d7 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Skull.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt index ddb1fe5558..a8aa8bd358 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Tunestr.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt index 3cff5f596f..338eef3642 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Weed.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt index 7b3631fc7c..3924aaa01b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/hashtags/Zap.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Bookmark.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Bookmark.kt index 89684d1c98..1a32595294 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Bookmark.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Bookmark.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.graphics.StrokeCap.Companion.Butt import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Following.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Following.kt index 583ebfd724..326474d8a8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Following.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Following.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt index 1bcf1a6b74..52a15572b9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Like.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt index fd4eb5fb06..d41cc91b37 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Liked.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt index 533fc64f72..b3775729f0 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reply.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt index 32d7268384..3d2025d7e9 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Repost.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt index f3e0726d63..5d99988b47 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Reposted.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt index bab4cda6f5..34c17c3b9d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Search.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt index 40156d93cf..345dd88af1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Share.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt index a56999f41d..73ee2a4d2b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/Zap.kt @@ -31,7 +31,7 @@ import androidx.compose.ui.graphics.vector.DefaultFillType import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.PathBuilder import androidx.compose.ui.graphics.vector.path -import org.jetbrains.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt index ba3d835675..252b342653 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/icons/ZapSplit.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.graphics.vector.DefaultFillType import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.PathBuilder import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt index a5c197bb65..6a493dd04a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/RobohashAssembler.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.robohash.parts.accessory0Seven import com.vitorpamplona.amethyst.commons.robohash.parts.accessory1Nose @@ -86,7 +87,6 @@ import com.vitorpamplona.amethyst.commons.robohash.parts.mouth9Closed import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.sha256.sha256 -import org.jetbrains.compose.ui.tooling.preview.Preview val Black = SolidColor(Color.Black) val Gray = SolidColor(Color(0xFF6d6e70)) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory0Seven.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory0Seven.kt index f2261f6fde..e98d7c67d8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory0Seven.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory0Seven.kt @@ -29,10 +29,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory1Nose.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory1Nose.kt index 23c05187af..349a44766e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory1Nose.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory1Nose.kt @@ -27,9 +27,9 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory2HornRed.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory2HornRed.kt index c02c969f46..0faf7f8398 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory2HornRed.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory2HornRed.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Brown import com.vitorpamplona.amethyst.commons.robohash.LightBrown @@ -34,7 +35,6 @@ import com.vitorpamplona.amethyst.commons.robohash.LightGray import com.vitorpamplona.amethyst.commons.robohash.LightRed import com.vitorpamplona.amethyst.commons.robohash.MediumGray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory3Button.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory3Button.kt index 230f701c2f..ae1054843f 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory3Button.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory3Button.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.LightRed import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory4Satellite.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory4Satellite.kt index 0dbbc39b09..cb093dfbb4 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory4Satellite.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory4Satellite.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.LightRed import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory5Mustache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory5Mustache.kt index fcff3c8ba8..d376618300 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory5Mustache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Accessory5Mustache.kt @@ -27,9 +27,9 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body0Trooper.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body0Trooper.kt index 4adcd72bc0..f07e5875df 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body0Trooper.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body0Trooper.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt index 1b50a772f5..992e24088e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body1Thin.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body2Thinnest.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body2Thinnest.kt index c5c13ff25d..ad57bc5ec1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body2Thinnest.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body2Thinnest.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt index c403b90546..52597a40d8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body3Front.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt index 6f3335c7c4..dadf2805fd 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body4Round.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt index 3d2219eabc..515a8c5ca1 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body5Neck.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body6Ironman.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body6Ironman.kt index 635c438e5b..dc56fa1ef4 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body6Ironman.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body6Ironman.kt @@ -27,11 +27,11 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.Yellow import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body7Neckthinner.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body7Neckthinner.kt index 0200c6737e..20f50c631b 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body7Neckthinner.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body7Neckthinner.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt index 046f6c26ca..e0218819e3 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body8Big.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt index 2c58e6d837..3742241bfa 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/robohash/parts/Body9Huge.kt @@ -27,10 +27,10 @@ import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector.Builder import androidx.compose.ui.graphics.vector.PathData import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview import com.vitorpamplona.amethyst.commons.robohash.Black import com.vitorpamplona.amethyst.commons.robohash.Gray import com.vitorpamplona.amethyst.commons.robohash.roboBuilder -import org.jetbrains.compose.ui.tooling.preview.Preview @Preview @Composable From 04b4429809b1a51be79fafe92a374d3a200eaa70 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 20:57:32 -0400 Subject: [PATCH 128/172] removes another deprecation --- .../commons/ui/components/BunkerHeartbeatIndicator.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/BunkerHeartbeatIndicator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/BunkerHeartbeatIndicator.kt index 7c295ef918..9a3f43f22c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/BunkerHeartbeatIndicator.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/components/BunkerHeartbeatIndicator.kt @@ -33,6 +33,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text +import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.rememberTooltipState @@ -69,14 +70,10 @@ fun BunkerHeartbeatIndicator( is SignerConnectionState.Disconnected -> { "Bunker disconnected" } - - else -> { - "" - } } TooltipBox( - positionProvider = TooltipDefaults.rememberTooltipPositionProvider(), + positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above), tooltip = { PlainTooltip { Text(tooltipText) } }, state = tooltipState, modifier = modifier, @@ -110,8 +107,6 @@ fun BunkerHeartbeatIndicator( modifier = Modifier.size(20.dp), ) } - - else -> {} } } } From d426dbd4ffff1dd1a28ea2554ff453623bb24f54 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 21:10:59 -0400 Subject: [PATCH 129/172] adds icons for desktop releases --- desktopApp/src/jvmMain/resources/icon.icns | Bin 0 -> 104193 bytes desktopApp/src/jvmMain/resources/icon.ico | Bin 0 -> 4286 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 desktopApp/src/jvmMain/resources/icon.icns create mode 100644 desktopApp/src/jvmMain/resources/icon.ico diff --git a/desktopApp/src/jvmMain/resources/icon.icns b/desktopApp/src/jvmMain/resources/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..c46a41b0b08515483e3c5e9323a9ccfccd2385e3 GIT binary patch literal 104193 zcmeFaRa9KTwl3Tajk~)AcXxM};O_43?(Tu$5P~EG3+{~vw*+@5xI=(&JA3c5&pH2p z?!$e!W88;yQ>&_0%~`cp*BYzpo3p;c&cfLP0L%v3S-5fm04PRrYAUiQh=hm$NJd^x zN*xlT{|WHWkRu&4ng##>478J!RI{_NfSgDRN)wjr7b6}t(U7LciNVEogI6EyWntLX ztx}6bEYr6A(Yrg#YCD_yv6T<81_n+Pw+fYIvR$ARF?(b<>uwvo{ z&EdvK#ucI!gp`TjY9?JpeHlYMV4k!u8`8huWkud^dS_KLM=S%Cpeup+#`3$G3=Hmv zM*ENN<>rD{`$cyjG{*N3}DTlM#v z^6yeJd-KZ2-+h|ob*>NjH1quOd$9GpEJIkRZ{5+)tNowXwi$zL-p!}ikH24j$Jl+8 zp%3e9`F?(JU)8rx>C#X%u(>+3ZNL_xFP^f!d9u25?pr=Bl`{B!cvYJ|*h1;u#l@X} ze5ZhE1&>pmU~mU@NO#xrNuXiAGJEvGkA1iBRz8z*QO`!cz;>#TZmTczkgMydo06gp z$w=(z7+B*qDiif+;0-CFy(yK{Dad2bWhzO;S5adf@*^JeMi&H4RP zMeD-d{nPRJ?akevr=&rL=oJzHJ=Ik-0Al-#)_*eQin3CGSQrvHNCv@GPR|1XK<)bz zfH-stp^%esp7P4la6h5ZfrwO9`AaqsonC)UT~Bp0A97cB7i&95D{@aiS1a;=yc+nD z$}|j;i7Aku53u>eFB|w#q1V*ULc;Koua}qP@^mF)Ihx1H<$4Shd!j#?g`4S$=cRj7 znii&d3n!t#Ea7A9roO=eo(QcP0lz-JLA&ELpZ#KK3Dy$}C3=o9GWKE{B|40JnLLTf z3ij<`tird$9&I&^-c1TC88rRM0{1CL6`sd98O$lT(LY>&(Jm9l{K$8J-#NuIiupPCj$A(=L9o*28Xx1P@P@vkb~yu7

    !q@x=mToj1?5{xh;Lk4NH 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 E2RWzav;Y7A literal 0 HcmV?d00001 diff --git a/desktopApp/src/jvmMain/resources/icon.ico b/desktopApp/src/jvmMain/resources/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..61b727bfe589246364734e4732a334c138f6f99f GIT binary patch literal 4286 zcmeI0`BPI@6vxwlpwplFvp;n@ooZWf!?4LJq5^|PkX6N2EoxQV#|?3b8U$Gifq)B8 zG!e><1p%XB5n{rsB1?z}3K~g}L=wWc_a!v;H9;ge(_egm&RdKFc|oJ&3OhUbXrVk$LAc!+5p2n zkDwYFu-1J}e)P-uGT4Zx}ox<3#wzBAY?m0#9^X7-VTk4i_mg)1;&Ou zX)!Dby-n@7(G$Hr(uBIxycl(I8%Zb=Y0jzPFY_?bc5@T!l# zzOu&q$c^yEjR;p5`A&pyPxy9(zl87?621-L|6t%ZCfGpHRrOz-nLI3ejzpimIJY#| z$ai9)>}))F?g^7E^^77e2ic)+xUt1;M!x9$cWBEEflf1GiZR^}(X~>XUK9?tU5J4n z=Mn&EK{?HQboeFm_ijdZm^;aTW`6C(`50)*qQ$TzynUkrFYFXzY(v3bX5?S?3`S3b zm}ahgF@RgU!jKi>I`jOWnU5x}H`EIH`q2VT2~R{VIAgcfnEw<%VMQop;%1sTwHB>K zH&J}#XR`l%!QbkPykG56@Y^z!>|2WRLyJ&(%((t{3BT*FbC{f*q{Xl#^mp`v1Nsm=a}<*15rK-I|wXuYwE^ev5<-E@lf z$_HRGBTV&2@dZ&QFs@cl=VH>24n0FNpN%`G+{yggqUQ8s47QY*?tjdWw+baP!cUm? zKh=NI*Kfg4ckc(?$KNVZm$C=NQAjS2mLJFyq8&E%AeYQ37#yn z2hH=4OC^xiQ+rbnMc-qTk^Wn<&)9z|NdDb5X_mszsC(HZN0QG@Ty)p~ihtS79|cFE z&DNm)dN%5~N1)ef2>%gEDL(0cRQ(Gttb}Ut>5ME(1{0*Vi(^u7fl2y5mA}V2q^%8x zTq32JYe`MKBlAvOgKf2h?K;I@h*rT#(ld25``HMcA~E7UHcs(bNb>SS#YGN=ySvdX zs)1Zw2gS1@lAL(E?e@aOGHE_2>s~I(mIa@{J%@R}o$=?1*_3=fz z|7u(b@W%CEFXR~d|G&<^-Q@f`z=W(;Fk6&G``7a4;BAOBoc~6?A9((2-t)sf=;t5l zzcuUtjJ^6a@8Z-sTM^48^}iM$%Wp48u$YUrN?B7gdH)JQlBXY1R{J2`Zxu3GUbwcw zgPi}a$lbIY{9hc&`NKrnVK4ID*J>@wqTl$K8cqBH+~a0I#LGb46&~uZrQtyq7fm^- ic$k|42|pQ9K_a?p1fI(((-a^M8Q<>-%q9UK#)Z literal 0 HcmV?d00001 From 78ffd5ec6bdfa988b2d95a4fa085295d20c61ab4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 15 Mar 2026 21:32:02 -0400 Subject: [PATCH 130/172] Moves nip06 tests to the right package. --- .../nip06KeyDerivation}/Bip32SeedDerivationCommonTest.kt | 5 +---- .../quartz/nip06KeyDerivation}/Nip06CommonTest.kt | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) rename quartz/src/commonTest/kotlin/{nip06KeyDerivationCommon => com/vitorpamplona/quartz/nip06KeyDerivation}/Bip32SeedDerivationCommonTest.kt (93%) rename quartz/src/commonTest/kotlin/{nip06KeyDerivationCommon => com/vitorpamplona/quartz/nip06KeyDerivation}/Nip06CommonTest.kt (97%) diff --git a/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationCommonTest.kt similarity index 93% rename from quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationCommonTest.kt index 4a6ccb8f98..c9f3040b06 100644 --- a/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Bip32SeedDerivationCommonTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Bip32SeedDerivationCommonTest.kt @@ -18,12 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package nip06KeyDerivationCommon +package com.vitorpamplona.quartz.nip06KeyDerivation import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip06KeyDerivation.Bip32SeedDerivation -import com.vitorpamplona.quartz.nip06KeyDerivation.Bip39Mnemonics -import com.vitorpamplona.quartz.nip06KeyDerivation.KeyPath import kotlin.test.Test import kotlin.test.assertEquals diff --git a/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06CommonTest.kt similarity index 97% rename from quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt rename to quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06CommonTest.kt index 5feaa16216..eec79ca775 100644 --- a/quartz/src/commonTest/kotlin/nip06KeyDerivationCommon/Nip06CommonTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip06KeyDerivation/Nip06CommonTest.kt @@ -18,10 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package nip06KeyDerivationCommon +package com.vitorpamplona.quartz.nip06KeyDerivation import com.vitorpamplona.quartz.nip01Core.core.toHexKey -import com.vitorpamplona.quartz.nip06KeyDerivation.Nip06 import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals From e4b4606ab90033ed9aa36f89469177acd860d512 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Mon, 16 Mar 2026 07:35:09 +0200 Subject: [PATCH 131/172] fix: revert ReactionsSettingsScreen to upstream version Spotless reformatted this file during merge. Restoring upstream's version to avoid unintended changes to the mobile app. Co-Authored-By: Claude Opus 4.6 --- .../settings/ReactionsSettingsScreen.kt | 76 ++++++++++++------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt index 15b5e583c3..e995b13772 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/ReactionsSettingsScreen.kt @@ -21,8 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -31,10 +33,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DragHandle +import androidx.compose.material.icons.filled.DragIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -56,17 +57,32 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.ReactionRowAction import com.vitorpamplona.amethyst.model.ReactionRowItem +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size20dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow + +@Composable +@Preview(device = "spec:width=2100px,height=2340px,dpi=440") +fun ReactionsSettingsScreenPreview() { + ThemeComparisonRow { + ReactionsSettingsScreen( + mockAccountViewModel(), + EmptyNav(), + ) + } +} @Composable fun ReactionsSettingsScreen( @@ -87,7 +103,7 @@ fun ReactionsSettingsScreen( @Composable fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { val reactionRowItems by accountViewModel.reactionRowItemsFlow().collectAsStateWithLifecycle() - var items by remember(reactionRowItems) { mutableStateOf(reactionRowItems.toMutableList()) } + var items by remember(reactionRowItems) { mutableStateOf(reactionRowItems.toList()) } fun save(newItems: List) { items = newItems.toMutableList() @@ -97,13 +113,14 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { var draggedItemIndex by remember { mutableIntStateOf(-1) } var dragOffset by remember { mutableFloatStateOf(0f) } val itemHeights = remember { mutableStateMapOf() } + val isDragging = draggedItemIndex >= 0 + val scrollState = remember { ScrollState(0) } Column( modifier = Modifier .fillMaxSize() - .padding(horizontal = Size20dp) - .verticalScroll(rememberScrollState()), + .verticalScroll(scrollState, enabled = !isDragging), ) { Spacer(modifier = Modifier.height(16.dp)) @@ -111,7 +128,7 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { text = stringRes(R.string.reactions_settings_description), style = MaterialTheme.typography.bodyMedium, color = Color.Gray, - modifier = Modifier.padding(bottom = 16.dp), + modifier = Modifier.padding(bottom = 16.dp, start = Size20dp, end = Size20dp), ) items.forEachIndexed { index, item -> @@ -206,7 +223,7 @@ fun ReactionsSettingsContent(accountViewModel: AccountViewModel) { .zIndex(if (isDragging) 1f else 0f), ) if (index < items.lastIndex) { - HorizontalDivider() + HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp)) } } @@ -245,7 +262,18 @@ private fun ReactionRowItemCard( scaleX = 1.02f scaleY = 1.02f } - }.padding(vertical = 8.dp), + }.padding(vertical = 8.dp, horizontal = Size20dp) + .pointerInput(Unit) { + detectDragGestures( + onDragStart = { onDragStart() }, + onDrag = { change, dragAmount -> + change.consume() + onDrag(dragAmount.y) + }, + onDragEnd = { onDragEnd() }, + onDragCancel = { onDragCancel() }, + ) + }, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -268,25 +296,17 @@ private fun ReactionRowItemCard( ) } - Icon( - Icons.Default.DragHandle, - contentDescription = stringRes(R.string.reactions_settings_reorder), - modifier = - Modifier - .size(32.dp) - .pointerInput(Unit) { - detectDragGesturesAfterLongPress( - onDragStart = { onDragStart() }, - onDrag = { change, dragAmount -> - change.consume() - onDrag(dragAmount.y) - }, - onDragEnd = { onDragEnd() }, - onDragCancel = { onDragCancel() }, - ) - }, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Box( + modifier = Modifier.size(32.dp), + contentAlignment = Alignment.CenterEnd, + ) { + Icon( + Icons.Default.DragIndicator, + contentDescription = stringRes(R.string.reactions_settings_reorder), + modifier = Modifier.size(28.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } Spacer(modifier = Modifier.height(8.dp)) From 1c663cb26bde356dbfa11c039e483c48673a1af3 Mon Sep 17 00:00:00 2001 From: Nathan Day Date: Mon, 16 Mar 2026 10:32:18 +0000 Subject: [PATCH 132/172] Fix swipe-back triggering unwanted draft deletion signing When swiping back on a DM screen with an empty message, the BackHandler was calling sendDraftSync() which checks if the message is blank and then calls deleteDraftIgnoreErrors(). This creates and signs a NIP-09 DeletionEvent, prompting the external signer to sign a deletion event. The fix adds a check to only call sendDraftSync() when there's actual content in the message, avoiding the unnecessary signing prompt. --- .../chats/privateDM/send/PrivateMessageEditFieldRow.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 2615ac2ec6..18df26d779 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -88,10 +88,12 @@ fun PrivateMessageEditFieldRow( nav: INav, ) { BackHandler { - accountViewModel.launchSigner { - channelScreenModel.sendDraftSync() - channelScreenModel.cancel() + if (channelScreenModel.message.text.isNotBlank()) { + accountViewModel.launchSigner { + channelScreenModel.sendDraftSync() + } } + channelScreenModel.cancel() nav.popBack() } From be09535cd6ff447d610d0156fd224588ea13b13c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 16 Mar 2026 12:17:53 +0000 Subject: [PATCH 133/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-zh-rCN/strings.xml | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 6d7eafd38e..95b1bad1c2 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -673,6 +673,7 @@ 写入到继电器 向该中继发送事件和过滤请求所使用的数据量 从该中继接收事件和过滤响应所使用的数据量 + 已保存的事件 尝试从 %1$s 获取中继器信息时出错 机主 使用者 @@ -1052,6 +1053,29 @@ 全球 短篇 国际象棋 + 钱包 + 余额 + 发送 + 接收 + 交易 + 未连接钱包 + 在打闪设置中设置Nostr Wallet Connect (NWC) 连接以使用钱包。 + 设置钱包 + + 粘贴 BOLT-11 发票 + 付款 + 支付成功 + 正在发送付款… + 聪金额 + 描述(可选) + 创建发票 + 正在创建发票… + 复制发票 + 尚无交易 + 正在加载… + 已收到 + 已发送 + 刷新 安全滤镜 导入关注 新帖子 @@ -1099,6 +1123,13 @@ 取消打闪拆分 添加内容警告 移除内容警告 + 添加过期日期 + 删除过期日期 + 过期日期 + 帖子将在此日期后被客户端隐藏 (NIP-40) + 选择到期日期和时间 + 在 %1$s 过期 + 到期时间 将 npub 显示为二维码 以二维码显示 nprofile 地址无效 @@ -1168,6 +1199,9 @@ 中继黑名单 中继黑名单 应用永远不会连接的中继 + 导出中继设置 + 导出为文本 + 导出为 ZIP (JSON) 打闪开发人员! 你的捐赠帮助我们做出不同的贡献。每个聪都很重要! 立即捐款 @@ -1295,6 +1329,7 @@ 找到了崩溃报告 要用私信将最近的崩溃报告发送给 Amethyst 吗?不会分享个人信息 发送它 + 此消息将在 %1$s 后消失 此消息将在 %1$d 天内消失 选择签名者 已经在列表中 @@ -1523,4 +1558,9 @@ 全选 %1$d%% 运行时间 Namecoin 设置 + 比特币资源管理器 (OTS) + 事件 + 私信 + 个人资料 + 中继设置 From 714afdc2a8c04092e4be60af26b88000134045dc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 09:35:41 -0400 Subject: [PATCH 134/172] Update dependencies --- gradle/libs.versions.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 998b19aa04..e232d95ddc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] accompanistAdaptive = "0.37.3" cachemapVersion = "0.2.4" -composeMultiplatform = "1.10.1" -activityCompose = "1.12.4" +composeMultiplatform = "1.10.2" +activityCompose = "1.13.0" agp = "9.1.0" android-compileSdk = "36" android-minSdk = "26" @@ -11,13 +11,13 @@ androidKotlinGeohash = "b481c6a64e" androidxJunit = "1.3.0" appcompat = "1.7.1" audiowaveform = "1.1.2" -benchmark = "1.5.0-alpha03" +benchmark = "1.5.0-alpha04" biometricKtx = "1.2.0-alpha05" coil = "3.4.0" -composeBom = "2026.02.01" -composeRuntimeAnnotation = "1.10.4" -coreKtx = "1.17.0" -datastore = "1.2.0" +composeBom = "2026.03.00" +composeRuntimeAnnotation = "1.10.5" +coreKtx = "1.18.0" +datastore = "1.2.1" devWhyolegCryptography = "0.5.0" espressoCore = "3.7.0" firebaseBom = "34.10.0" @@ -53,7 +53,7 @@ secp256k1KmpJniAndroid = "0.22.0" securityCryptoKtx = "1.1.0" spotless = "8.3.0" tarsosdsp = "2.5" -torAndroid = "0.4.9.5" +torAndroid = "0.4.9.5.1" translate = "17.0.3" jetbrainsCompose = "1.10.2" unifiedpush = "3.0.10" @@ -68,7 +68,7 @@ androidxCollection = "1.5.0" kotlinTest = "2.3.0" core = "1.7.0" mavenPublish = "0.36.0" -spmForKmpVersion = "1.4.9" +spmForKmpVersion = "1.4.10" [libraries] abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor-enhanced" } From 2a9c9e4510d807d8528abfa64054010f0417d77a Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 15 Mar 2026 23:25:04 +0100 Subject: [PATCH 135/172] fix: allow swipe-to-close when drawer is open on pager screens Keep drawer gestures enabled when the drawer is open so users can swipe left to close it. Only disable gestures when the drawer is closed (to let the pager and ZonedSwipeModifier handle gestures). --- .../ui/components/ZonedSwipeModifier.kt | 90 +++++++++++++++++++ .../amethyst/ui/navigation/AppNavigation.kt | 11 ++- .../AccountSwitcherAndLeftDrawerLayout.kt | 2 + .../chats/rooms/feed/ChatroomListTabs.kt | 8 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 8 +- 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt new file mode 100644 index 0000000000..440a8d9358 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.components + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp + +private val EDGE_ZONE_WIDTH = 48.dp + +fun Modifier.zonedDrawerSwipe( + pagerState: PagerState, + openDrawer: () -> Unit, +): Modifier = + composed { + val edgeZonePx = with(LocalDensity.current) { EDGE_ZONE_WIDTH.toPx() } + + var gestureStartX by remember { mutableFloatStateOf(0f) } + var gestureStartPage by remember { mutableIntStateOf(0) } + var drawerOpened by remember { mutableStateOf(false) } + + val connection = + remember { + object : NestedScrollConnection { + override fun onPreScroll( + available: Offset, + source: NestedScrollSource, + ): Offset { + if (source != NestedScrollSource.UserInput) return Offset.Zero + if (drawerOpened) return Offset(available.x, 0f) + + // available.x > 0 means user is swiping right + if (available.x > 0f) { + val wasOnFirstPage = gestureStartPage == 0 + val isInEdgeZone = gestureStartX < edgeZonePx + + if (wasOnFirstPage || !isInEdgeZone) { + drawerOpened = true + openDrawer() + return Offset(available.x, 0f) + } + } + return Offset.Zero + } + } + } + + this + .pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + gestureStartX = down.position.x + gestureStartPage = pagerState.currentPage + drawerOpened = false + } + }.nestedScroll(connection) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index f4b276b432..d91803756a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -37,8 +37,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.core.util.Consumer +import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.service.crashreports.DisplayCrashMessages @@ -142,7 +144,14 @@ fun AppNavigation( ) { val nav = rememberNav() - AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav) { + val navBackStackEntry by nav.controller.currentBackStackEntryAsState() + val isTabPagerRoute = + navBackStackEntry?.destination?.let { dest -> + dest.hasRoute() || dest.hasRoute() + } ?: false + val drawerGesturesEnabled = !isTabPagerRoute || nav.drawerState.isOpen + + AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav, drawerGesturesEnabled) { NavHost( navController = nav.controller, startDestination = Route.Home, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt index 84ec07af70..8dc7d01f91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountSwitcherAndLeftDrawerLayout.kt @@ -49,6 +49,7 @@ fun AccountSwitcherAndLeftDrawerLayout( accountViewModel: AccountViewModel, accountSessionManager: AccountSessionManager, nav: INav, + gesturesEnabled: Boolean = true, content: @Composable () -> Unit, ) { val scope = rememberCoroutineScope() @@ -83,6 +84,7 @@ fun AccountSwitcherAndLeftDrawerLayout( ModalNavigationDrawer( drawerState = nav.drawerState, + gesturesEnabled = gesturesEnabled, drawerContent = { DrawerContent(nav, openSheetFunction, accountViewModel) BackHandler(enabled = nav.drawerState.isOpen, nav::closeDrawer) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt index 9f48375a48..5bf51f5080 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/feed/ChatroomListTabs.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState +import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -124,7 +125,12 @@ fun MessagesPager( HorizontalPager( contentPadding = paddingValues, state = pagerState, - userScrollEnabled = false, + userScrollEnabled = true, + modifier = + Modifier.zonedDrawerSwipe( + pagerState = pagerState, + openDrawer = nav::openDrawer, + ), ) { page -> ChatroomListFeedView( feedContentState = tabs[page].feedContentState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 86891c7fe9..feaeb15436 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.model.TopFilter import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.components.zonedDrawerSwipe import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedContentState import com.vitorpamplona.amethyst.ui.feeds.ChannelFeedState import com.vitorpamplona.amethyst.ui.feeds.PagerStateKeys @@ -211,7 +212,12 @@ private fun HomePages( HorizontalPager( contentPadding = it, state = pagerState, - userScrollEnabled = false, + userScrollEnabled = true, + modifier = + Modifier.zonedDrawerSwipe( + pagerState = pagerState, + openDrawer = nav::openDrawer, + ), ) { page -> HomeFeeds( feedState = tabs[page].feedState, From 844847d87194148ee14c175ae3942d8a715b9b57 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 16 Mar 2026 14:45:10 +0100 Subject: [PATCH 136/172] use 50/50 split for pager vs dock zones --- .../amethyst/ui/components/ZonedSwipeModifier.kt | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt index 440a8d9358..2579251446 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt @@ -36,18 +36,16 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.dp +import androidx.compose.ui.layout.onSizeChanged -private val EDGE_ZONE_WIDTH = 48.dp +private const val PAGER_ZONE_FRACTION = 0.5f fun Modifier.zonedDrawerSwipe( pagerState: PagerState, openDrawer: () -> Unit, ): Modifier = composed { - val edgeZonePx = with(LocalDensity.current) { EDGE_ZONE_WIDTH.toPx() } - + var widthPx by remember { mutableFloatStateOf(1f) } var gestureStartX by remember { mutableFloatStateOf(0f) } var gestureStartPage by remember { mutableIntStateOf(0) } var drawerOpened by remember { mutableStateOf(false) } @@ -65,9 +63,9 @@ fun Modifier.zonedDrawerSwipe( // available.x > 0 means user is swiping right if (available.x > 0f) { val wasOnFirstPage = gestureStartPage == 0 - val isInEdgeZone = gestureStartX < edgeZonePx + val isInPagerZone = gestureStartX < widthPx * PAGER_ZONE_FRACTION - if (wasOnFirstPage || !isInEdgeZone) { + if (wasOnFirstPage || !isInPagerZone) { drawerOpened = true openDrawer() return Offset(available.x, 0f) @@ -79,6 +77,7 @@ fun Modifier.zonedDrawerSwipe( } this + .onSizeChanged { widthPx = it.width.toFloat() } .pointerInput(Unit) { awaitEachGesture { val down = awaitFirstDown(requireUnconsumed = false) From 588cfe4c96465865e27973b5c633738a64d6978c Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 16 Mar 2026 16:23:08 +0100 Subject: [PATCH 137/172] =?UTF-8?q?Unnecessary=20=3F.=20/=20=3F:=20/=20!!?= =?UTF-8?q?=20on=20non-null=20Missing=20@OptIn=20annotations=20Parameter?= =?UTF-8?q?=20name=20mismatches=20Icons.Filled.*=20=20/=20Icons.AutoMirror?= =?UTF-8?q?ed.*=20No=20cast=20needed=20/=20remove=20inline=20LocalLifecycl?= =?UTF-8?q?eOwner=20import=20fix=20NIP-51=20name()=20=E2=86=92=20title()?= =?UTF-8?q?=20Chess=20gameId=20=E2=86=92=20startEventId=20Nullable=20Decim?= =?UTF-8?q?alFormat=20safe=20calls=20DelicateCoroutinesApi=20opt-in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vitorpamplona/amethyst/model/Account.kt | 1 + .../amethyst/model/LocalCache.kt | 19 ++++++++++--------- .../nip47WalletConnect/NwcSignerState.kt | 2 +- .../LabeledBookmarkListsState.kt | 4 ++-- .../nip51Lists/peopleList/PeopleListsState.kt | 4 ++-- .../serverList/MergedFollowListsState.kt | 1 + .../amethyst/service/cashu/v4/V4Models.kt | 4 ++++ .../watchers/EventWatcherSubAssembler.kt | 6 +++--- .../mediaServers/BlossomServersViewModel.kt | 2 +- .../amethyst/ui/note/BadgeCompose.kt | 2 +- .../amethyst/ui/note/UsernameDisplay.kt | 2 +- .../ui/note/ZapFormatterNoDecimals.kt | 8 ++++---- .../amethyst/ui/note/ZapPollNoteViewModel.kt | 4 ++-- .../amethyst/ui/note/types/Chess.kt | 2 +- .../amethyst/ui/note/types/FileStorage.kt | 4 ++-- .../ui/note/types/TextModification.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 4 ++-- .../nip99Classifieds/NewProductViewModel.kt | 17 +++++++---------- .../NewPublicMessageViewModel.kt | 6 +++--- .../UserProfileFollowersUserFeedViewModel.kt | 1 + .../UserProfileFollowsUserFeedViewModel.kt | 1 + .../loggedIn/profile/header/apps/WatchApp.kt | 2 +- .../zaps/dal/UserProfileZapsViewModel.kt | 1 + .../loggedIn/relays/RelayInformationScreen.kt | 4 ++-- .../loggedIn/search/SearchBarViewModel.kt | 1 + .../settings/SecurityFiltersScreen.kt | 2 +- .../screen/loggedIn/wallet/WalletViewModel.kt | 2 +- .../amethyst/commons/chess/LiveChessGame.kt | 4 ++-- .../amethyst/commons/chess/MoveNavigator.kt | 8 ++++---- .../amethyst/commons/model/Note.kt | 2 +- 30 files changed, 65 insertions(+), 57 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ca19be854e..0c5f5c0844 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -2016,6 +2016,7 @@ class Account( } scope.launch(Dispatchers.IO) { + @OptIn(kotlinx.coroutines.FlowPreview::class) settings.saveable.debounce(1000).collect { if (it.accountSettings != null) { LocalPreferences.saveToEncryptedStorage(it.accountSettings) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 750d10ae9c..923e7a91b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -358,19 +358,19 @@ object LocalCache : ILocalCache, ICacheProvider { fun load(keys: Set): Set = keys.mapNotNullTo(mutableSetOf(), ::checkGetOrCreateUser) - override fun getOrCreateUser(key: HexKey): User { - require(isValidHex(key = key)) { "$key is not a valid hex" } + override fun getOrCreateUser(pubkey: HexKey): User { + require(isValidHex(key = pubkey)) { "$pubkey is not a valid hex" } - return users.getOrCreate(key) { - val nip65RelayListNote = getOrCreateAddressableNoteInternal(AdvertisedRelayListEvent.createAddress(key)) - val dmRelayListNote = getOrCreateAddressableNoteInternal(ChatMessageRelayListEvent.createAddress(key)) + return users.getOrCreate(pubkey) { + val nip65RelayListNote = getOrCreateAddressableNoteInternal(AdvertisedRelayListEvent.createAddress(pubkey)) + val dmRelayListNote = getOrCreateAddressableNoteInternal(ChatMessageRelayListEvent.createAddress(pubkey)) User(it, nip65RelayListNote, dmRelayListNote) } } - override fun getUserIfExists(key: String): User? { - if (key.isEmpty()) return null - return users.get(key) + override fun getUserIfExists(pubkey: String): User? { + if (pubkey.isEmpty()) return null + return users.get(pubkey) } override fun countUsers(predicate: (String, User) -> Boolean): Int { @@ -394,7 +394,7 @@ object LocalCache : ILocalCache, ICacheProvider { fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address) - override fun getNoteIfExists(key: String): Note? = if (key.length == 64) notes.get(key) else Address.parse(key)?.let { addressables.get(it) } + override fun getNoteIfExists(hexKey: String): Note? = if (hexKey.length == 64) notes.get(hexKey) else Address.parse(hexKey)?.let { addressables.get(it) } fun getNoteIfExists(key: ETag): Note? = notes.get(key.eventId) @@ -2250,6 +2250,7 @@ object LocalCache : ILocalCache, ICacheProvider { requestNote?.let { request -> zappedNote?.addZapPayment(request, note) } + @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { responseCallback(event) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt index 206f9bbfd6..aa499a6b95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip47WalletConnect/NwcSignerState.kt @@ -114,7 +114,7 @@ class NwcSignerState( fun hasWalletConnectSetup(): Boolean = nip47Setup.value != null - override fun isNIP47Author(pubkey: HexKey?): Boolean = nip47Signer.value.pubKey == pubkey + override fun isNIP47Author(pubKey: HexKey?): Boolean = nip47Signer.value.pubKey == pubKey /** * Decrypts a NIP-47 payment request using the current signer. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt index 91f5fc1ac0..54c03aec62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/labeledBookmarkLists/LabeledBookmarkListsState.kt @@ -36,7 +36,7 @@ import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.BookmarkIdTag import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.LabeledBookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.description import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.image -import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.name +import com.vitorpamplona.quartz.nip51Lists.labeledBookmarkList.title import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -181,7 +181,7 @@ class LabeledBookmarkListsState( val template = listEvent.update { - if (listName != null) name(listName) + if (listName != null) title(listName) if (listDescription != null) description(listDescription) if (listImage != null) image(listImage) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt index 3787caac9f..d279ec20e5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip51Lists/peopleList/PeopleListsState.kt @@ -38,7 +38,7 @@ import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.description import com.vitorpamplona.quartz.nip51Lists.peopleList.image -import com.vitorpamplona.quartz.nip51Lists.peopleList.name +import com.vitorpamplona.quartz.nip51Lists.peopleList.title import com.vitorpamplona.quartz.utils.flattenToSet import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -226,7 +226,7 @@ class PeopleListsState( val template = listEvent.update { - if (listName != null) name(listName) + if (listName != null) title(listName) if (listDescription != null) description(listDescription) if (listImage != null) image(listImage) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt index f3d7ae385c..dfb319822e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/serverList/MergedFollowListsState.kt @@ -79,6 +79,7 @@ class MergedFollowListsState( communities = community.mapTo(mutableSetOf()) { it.address.toValue() }, ) + @OptIn(kotlinx.coroutines.FlowPreview::class) val flow: StateFlow = combine( listOf( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Models.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Models.kt index 5f2b176905..efd6e12fd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Models.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/cashu/v4/V4Models.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service.cashu.v4 +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.cbor.ByteString @@ -34,6 +35,7 @@ class V4Token( val t: Array?, ) +@OptIn(ExperimentalSerializationApi::class) @Serializable class V4T( // identifier @@ -42,6 +44,7 @@ class V4T( val p: Array, ) +@OptIn(ExperimentalSerializationApi::class) @Serializable class V4Proof( // amount @@ -57,6 +60,7 @@ class V4Proof( val w: String? = null, ) +@OptIn(ExperimentalSerializationApi::class) @Serializable class V4DleqProof( @ByteString diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt index be3e7e2527..91358cdfd0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/event/watchers/EventWatcherSubAssembler.kt @@ -51,14 +51,14 @@ class EventWatcherSubAssembler( } override fun updateFilter( - key: List, + keys: List, since: SincePerRelayMap?, ): List? { - if (key.isEmpty()) { + if (keys.isEmpty()) { return null } - lastNotesOnFilter = key.map { it.note } + lastNotesOnFilter = keys.map { it.note } return groupByRelayPresence(lastNotesOnFilter, latestEOSEs) .map { group -> diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt index 84142984be..23a5e561dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/mediaServers/BlossomServersViewModel.kt @@ -53,7 +53,7 @@ class BlossomServersViewModel : ViewModel() { fun refresh() { isModified = false _fileServers.update { - val obtainedFileServers = obtainFileServers() ?: emptyList() + val obtainedFileServers = obtainFileServers() obtainedFileServers.mapNotNull { serverUrl -> try { ServerName( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index ac0e28646c..321187de84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -62,7 +62,7 @@ fun BadgeCompose( nav: INav, ) { val noteState by observeNote(likeSetCard.note, accountViewModel) - val note = noteState?.note + val note = noteState.note val context = LocalContext.current.applicationContext diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index 2e1de3e286..14c5ed8990 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -30,10 +30,10 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.compose.LocalLifecycleOwner import com.vitorpamplona.amethyst.commons.model.ImmutableListOfLists import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt index 2856360eec..ab8aa20159 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapFormatterNoDecimals.kt @@ -49,10 +49,10 @@ fun showAmountInteger(amount: BigDecimal?): String { if (amount.abs() < BigDecimal(0.01)) return "" return when { - amount >= OneGiga -> dfG.get().format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) - amount >= OneMega -> dfM.get().format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) - amount >= TenKilo -> dfK.get().format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) - else -> dfN.get().format(amount) + amount >= OneGiga -> dfG.get()?.format(amount.div(OneGiga).setScale(0, RoundingMode.HALF_UP)) ?: "" + amount >= OneMega -> dfM.get()?.format(amount.div(OneMega).setScale(0, RoundingMode.HALF_UP)) ?: "" + amount >= TenKilo -> dfK.get()?.format(amount.div(OneKilo).setScale(0, RoundingMode.HALF_UP)) ?: "" + else -> dfN.get()?.format(amount) ?: "" } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt index 9d4b31d4a3..62d8be2b70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapPollNoteViewModel.kt @@ -118,13 +118,13 @@ class PollNoteViewModel : ViewModel() { it.zappedValue.value = zappedValue it.tally.value = tallyValue.toFloat() it.consensusThreadhold.value = consensusThreshold != null && tallyValue >= consensusThreshold!! - it.zappedByLoggedIn.value = account?.userProfile()?.let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } ?: false + it.zappedByLoggedIn.value = account.userProfile().let { it1 -> cachedIsPollOptionZappedBy(it.option, it1) } } } } fun checkIfCanZap(): Boolean { - val account = account ?: return false + val account = account val note = pollNote ?: return false return account.userProfile() != note.author && !wasZappedByLoggedInAccount } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt index 4840d06a1e..066de85c41 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Chess.kt @@ -96,7 +96,7 @@ fun RenderLiveChessChallenge( nav: INav, ) { val event = (note.event as? LiveChessGameChallengeEvent) ?: return - val gameId = event.gameId() ?: return + val gameId = event.gameId() val chessViewModel: ChessViewModelNew = viewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt index 6cdcec848d..29cb0937af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/FileStorage.kt @@ -72,9 +72,9 @@ private fun ObserverAndRenderNIP95( val content by remember(noteState) { // Creates a new object when the event arrives to force an update of the image. - val note = noteState?.note + val note = noteState.note val uri = header.toNostrUri() - val localDir = note?.idHex?.let { File(Amethyst.instance.nip95cache, it) } + val localDir = note.idHex.let { File(Amethyst.instance.nip95cache, it) } val blurHash = eventHeader.blurhash() val dimensions = eventHeader.dimensions() val description = eventHeader.alt() ?: eventHeader.content diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt index d69e4a5a07..e7772e9c02 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/TextModification.kt @@ -163,7 +163,7 @@ fun RenderTextModificationEvent( } LaunchedEffect(key1 = noteState) { - val newAuthor = accountViewModel.isLoggedUser(noteState?.note?.author) + val newAuthor = accountViewModel.isLoggedUser(noteState.note.author) if (isAuthorTheLoggedUser.value != newAuthor) { isAuthorTheLoggedUser.value = newAuthor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 310d5044a6..b1c6267208 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -104,7 +104,7 @@ class ChannelMetadataViewModel : ViewModel() { fun createOrUpdate(onDone: (PublicChatChannel) -> Unit) { viewModelScope.launch(Dispatchers.IO) { - account?.let { account -> + account.let { account -> val channel = originalChannel if (channel == null) { val template = @@ -204,7 +204,7 @@ class ChannelMetadataViewModel : ViewModel() { onUploaded: (String) -> Unit, onError: (String, String) -> Unit, ) { - val account = account ?: return + val account = account onUploading(true) val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index c3d359d086..66b46d0a7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -220,7 +220,7 @@ open class NewProductViewModel : } open fun quote(quote: Note) { - val accountViewModel = accountViewModel ?: return + val accountViewModel = accountViewModel message = TextFieldValue(message.text + "\nnostr:${quote.toNEvent()}") @@ -307,7 +307,6 @@ open class NewProductViewModel : } suspend fun sendPostSync() { - val accountViewModel = accountViewModel ?: return val template = createTemplate() ?: return val version = draftTag.current @@ -320,8 +319,6 @@ open class NewProductViewModel : } suspend fun sendDraftSync() { - val accountViewModel = accountViewModel ?: return - if (message.text.isBlank()) { accountViewModel.account.deleteDraftIgnoreErrors(draftTag.current) } else { @@ -331,7 +328,7 @@ open class NewProductViewModel : } private suspend fun createTemplate(): EventTemplate? { - val accountViewModel = accountViewModel ?: return null + val accountViewModel = accountViewModel val tagger = NewMessageTagger( @@ -340,7 +337,7 @@ open class NewProductViewModel : ) tagger.run() - val emojis = findEmoji(tagger.message, account?.emoji?.myEmojis?.value) + val emojis = findEmoji(tagger.message, account.emoji.myEmojis.value) val urls = findURLs(tagger.message) val usedAttachments = iMetaDescription.filterIsIn(urls.toSet()) + productImages.map { it.toIMeta() } @@ -399,7 +396,7 @@ open class NewProductViewModel : context: Context, ) { viewModelScope.launch(Dispatchers.IO) { - val myAccount = account ?: return@launch + val myAccount = account val myMultiOrchestrator = multiOrchestrator ?: return@launch mediaUploadTracker.startUpload(myMultiOrchestrator.hasNonMedia()) @@ -501,8 +498,8 @@ open class NewProductViewModel : this.multiOrchestrator?.remove(selected) } - override fun updateMessage(it: TextFieldValue) { - message = it + override fun updateMessage(newMessage: TextFieldValue) { + message = newMessage urlPreviews.update(message) if (message.selection.collapsed) { @@ -616,7 +613,7 @@ open class NewProductViewModel : override fun updateZapFromText() { viewModelScope.launch(Dispatchers.IO) { - val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel!!) + val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), accountViewModel) tagger.run() tagger.pTags?.forEach { taggedUser -> if (!forwardZapTo.value.items.any { it.key == taggedUser }) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index fbda70c24d..414c808c13 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -322,7 +322,7 @@ class NewPublicMessageViewModel : } suspend fun sendPostSync() { - val template = createTemplate() ?: return + val template = createTemplate() val extraNotesToBroadcast = mutableListOf() if (nip95attachments.isNotEmpty()) { @@ -354,7 +354,7 @@ class NewPublicMessageViewModel : broadcast.add(it.second) } - val template = createTemplate() ?: return + val template = createTemplate() accountViewModel.account.createAndSendDraftIgnoreErrors(draftTag.current, template, broadcast) } } @@ -459,7 +459,7 @@ class NewPublicMessageViewModel : if (state.result is UploadOrchestrator.OrchestratorResult.NIP95Result) { val nip95 = account.createNip95(state.result.bytes, headerInfo = state.result.fileHeader, alt, contentWarningReason) nip95attachments = nip95attachments + nip95 - val note = nip95.let { it1 -> account?.consumeNip95(it1.first, it1.second) } + val note = nip95.let { it1 -> account.consumeNip95(it1.first, it1.second) } note?.let { message = message.insertUrlAtCursor("nostr:" + it.toNEvent()) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt index a614105d4b..d931e2cdc7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/dal/UserProfileFollowersUserFeedViewModel.kt @@ -63,6 +63,7 @@ class UserProfileFollowersUserFeedViewModel( } } + @OptIn(kotlinx.coroutines.FlowPreview::class) val followersFlow: StateFlow> = account.cache .observeEvents(followerFilter) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt index dab5574840..0667d781cb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/dal/UserProfileFollowsUserFeedViewModel.kt @@ -55,6 +55,7 @@ class UserProfileFollowsUserFeedViewModel( return LocalCache.load(nonHiddenFollows).sortedWith(sortingModel) } + @OptIn(kotlinx.coroutines.FlowPreview::class) val followsFlow: StateFlow> = contactList .flow() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt index 8514dde91b..c55f6e580f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt @@ -56,7 +56,7 @@ fun WatchApp( LaunchedEffect(key1 = appState) { withContext(Dispatchers.IO) { - (appState?.note?.event as? AppDefinitionEvent)?.appMetaData()?.let { metaData -> + (appState.note.event as? AppDefinitionEvent)?.appMetaData()?.let { metaData -> metaData.picture?.ifBlank { null }?.let { newLogo -> if (newLogo != appLogo) appLogo = newLogo } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt index 1c95843c22..35fd0bd23b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/dal/UserProfileZapsViewModel.kt @@ -112,6 +112,7 @@ class UserProfileZapsViewModel( return results.map { (user, amount) -> ZapAmount(user, amount) }.sortedWith(sortingModel) } + @OptIn(kotlinx.coroutines.FlowPreview::class) val receivedZapAmountsByUser: StateFlow> = account.cache .observeEvents(zapsToUser) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt index 64ebae2953..91d26f9377 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/RelayInformationScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.material.icons.automirrored.filled.Feed import androidx.compose.material.icons.automirrored.filled.Label import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.automirrored.filled.Message +import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.filled.AttachMoney import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Code @@ -61,7 +62,6 @@ import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Payment import androidx.compose.material.icons.filled.PrivacyTip -import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Storage import androidx.compose.material.icons.filled.Tag import androidx.compose.material.icons.filled.Topic @@ -931,7 +931,7 @@ private fun OutboxEventsCard(eventIds: Set) { horizontalArrangement = Arrangement.spacedBy(4.dp), ) { Icon( - imageVector = Icons.Default.Send, + imageVector = Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(12.dp), tint = MaterialTheme.colorScheme.onTertiaryContainer, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt index 028b9eec37..d29bb70375 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/search/SearchBarViewModel.kt @@ -90,6 +90,7 @@ class SearchBarViewModel( val listState: LazyListState = LazyListState(0, 0) + @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) val directNip05Resolver: Flow = searchTerm .debounce(400) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt index 21760c45e1..1f2473402d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SecurityFiltersScreen.kt @@ -55,7 +55,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -63,6 +62,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt index ff670665df..0e79349305 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/WalletViewModel.kt @@ -99,7 +99,7 @@ class WalletViewModel : ViewModel() { fun init(account: Account) { this.account = account - _hasWalletSetup.value = account.nip47SignerState?.hasWalletConnectSetup() == true + _hasWalletSetup.value = account.nip47SignerState.hasWalletConnectSetup() } fun refreshWalletSetup() { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/LiveChessGame.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/LiveChessGame.kt index 2dba6f7601..c04a06f361 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/LiveChessGame.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/LiveChessGame.kt @@ -237,7 +237,7 @@ fun LiveChessGameScreen( ) { // Game info - use currentPosition.activeColor for turn display GameInfoHeader( - gameId = gameState.gameId, + gameId = gameState.startEventId, opponentName = opponentName, playerColor = gameState.playerColor, currentTurn = currentPosition.activeColor, @@ -463,7 +463,7 @@ private fun GameInfoHeader( // Show turn or game result when (gameStatus) { is GameStatus.Finished -> { - val result = (gameStatus as GameStatus.Finished).result + val result = gameStatus.result val resultText = when { result == GameResult.DRAW -> "Draw" diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/MoveNavigator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/MoveNavigator.kt index f9a1b51c3a..0f5cb96931 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/MoveNavigator.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/chess/MoveNavigator.kt @@ -25,8 +25,8 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowLeft -import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.SkipNext import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.material3.Icon @@ -82,7 +82,7 @@ fun MoveNavigator( enabled = currentMove > 0, ) { Icon( - Icons.Default.KeyboardArrowLeft, + Icons.AutoMirrored.Filled.KeyboardArrowLeft, contentDescription = "Previous move", tint = if (currentMove > 0) { @@ -106,7 +106,7 @@ fun MoveNavigator( enabled = currentMove < totalMoves, ) { Icon( - Icons.Default.KeyboardArrowRight, + Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Next move", tint = if (currentMove < totalMoves) { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt index a66d32babe..f9c3c3f373 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt @@ -1027,7 +1027,7 @@ public inline fun Iterable.filterEvents(predicate: (T) -> Boolean): Li return dest } -public inline fun Iterable.filterAuthoredEvents(pubkey: HexKey): List { +public fun Iterable.filterAuthoredEvents(pubkey: HexKey): List { if (this is Collection && isEmpty()) return emptyList() val dest = ArrayList() From 81ecaa82ffdd20b6a92e6757cfdbe689a7eb6592 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 12:53:54 -0400 Subject: [PATCH 138/172] Migrates to vico charts 3.0 --- amethyst/build.gradle | 2 - .../notifications/NotificationSummaryState.kt | 8 ++-- .../chart/AmountValueFormatter.kt | 6 +-- .../chart/CountAxisValueFormatter.kt | 6 +-- .../chart/LastWeekLabelFormatter.kt | 6 +-- .../loggedIn/notifications/chart/ShowChart.kt | 45 ++++++++++--------- gradle/libs.versions.toml | 9 ++-- 7 files changed, 41 insertions(+), 41 deletions(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index fe6c881a89..b6b3c6f2aa 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -336,9 +336,7 @@ dependencies { fdroidImplementation libs.unifiedpush // Charts - implementation libs.vico.charts.core implementation libs.vico.charts.compose - implementation libs.vico.charts.views implementation libs.vico.charts.m3 // GeoHash diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt index 105d71a939..f112a41737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/NotificationSummaryState.kt @@ -21,10 +21,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications import androidx.compose.runtime.Stable -import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel -import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel -import com.patrykandpatrick.vico.core.common.data.ExtraStore -import com.patrykandpatrick.vico.core.common.data.MutableExtraStore +import com.patrykandpatrick.vico.compose.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.compose.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.compose.common.data.ExtraStore +import com.patrykandpatrick.vico.compose.common.data.MutableExtraStore import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt index 34675af1ac..70a2444ea8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/AmountValueFormatter.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart import androidx.compose.runtime.Stable -import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext -import com.patrykandpatrick.vico.core.cartesian.axis.Axis -import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.compose.cartesian.CartesianMeasuringContext +import com.patrykandpatrick.vico.compose.cartesian.axis.Axis +import com.patrykandpatrick.vico.compose.cartesian.data.CartesianValueFormatter import com.vitorpamplona.amethyst.ui.note.showAmountIntegerWithZero import com.vitorpamplona.amethyst.ui.note.showAmountWithZero import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.ShowDecimals diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/CountAxisValueFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/CountAxisValueFormatter.kt index 7cc79fa6fe..50223df250 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/CountAxisValueFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/CountAxisValueFormatter.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart import androidx.compose.runtime.Stable -import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext -import com.patrykandpatrick.vico.core.cartesian.axis.Axis -import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.compose.cartesian.CartesianMeasuringContext +import com.patrykandpatrick.vico.compose.cartesian.axis.Axis +import com.patrykandpatrick.vico.compose.cartesian.data.CartesianValueFormatter import kotlin.math.roundToInt @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/LastWeekLabelFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/LastWeekLabelFormatter.kt index f438df0fdc..df1f61ef80 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/LastWeekLabelFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/LastWeekLabelFormatter.kt @@ -22,9 +22,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart import android.util.LruCache import androidx.compose.runtime.Stable -import com.patrykandpatrick.vico.core.cartesian.CartesianMeasuringContext -import com.patrykandpatrick.vico.core.cartesian.axis.Axis -import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.compose.cartesian.CartesianMeasuringContext +import com.patrykandpatrick.vico.compose.cartesian.axis.Axis +import com.patrykandpatrick.vico.compose.cartesian.data.CartesianValueFormatter import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.math.roundToInt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/ShowChart.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/ShowChart.kt index 961895f1c2..8c568b7afd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/ShowChart.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/chart/ShowChart.kt @@ -21,34 +21,34 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.chart import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.sp import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.axis.Axis +import com.patrykandpatrick.vico.compose.cartesian.axis.HorizontalAxis +import com.patrykandpatrick.vico.compose.cartesian.axis.VerticalAxis import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent -import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom -import com.patrykandpatrick.vico.compose.cartesian.axis.rememberEnd -import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart +import com.patrykandpatrick.vico.compose.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer.AreaFill.Companion.single +import com.patrykandpatrick.vico.compose.cartesian.layer.LineCartesianLayer.Line import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart -import com.patrykandpatrick.vico.compose.common.fill -import com.patrykandpatrick.vico.core.cartesian.axis.Axis -import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis -import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis -import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel -import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer -import com.patrykandpatrick.vico.core.common.shader.ShaderProvider +import com.patrykandpatrick.vico.compose.common.Fill import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.RoyalBlue -fun makeLine(color: Color): LineCartesianLayer.Line = - LineCartesianLayer.Line( - fill = LineCartesianLayer.LineFill.single(fill(color)), +fun makeLine(color: Color): Line = + Line( + fill = LineCartesianLayer.LineFill.single(Fill(color)), areaFill = - LineCartesianLayer.AreaFill.single( - fill( - ShaderProvider.verticalGradient( - color.copy(alpha = 0.4f).toArgb(), - Color.Transparent.toArgb(), - ), + single( + Fill( + brush = + Brush.verticalGradient( + colors = listOf(color.copy(alpha = 0.4f), Color.Transparent), + ), ), ), pointConnector = LineCartesianLayer.PointConnector.cubic(), @@ -84,7 +84,10 @@ fun ShowChart(model: CartesianChartModel) { ), endAxis = VerticalAxis.rememberEnd( - label = rememberAxisLabelComponent(color = BitcoinOrange), + label = + rememberAxisLabelComponent( + style = TextStyle(color = BitcoinOrange, fontSize = 12.sp), + ), valueFormatter = AmountValueFormatter(), itemPlacer = VerticalAxis.ItemPlacer.count({ 7 }), ), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e232d95ddc..a9963706c7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ torAndroid = "0.4.9.5.1" translate = "17.0.3" jetbrainsCompose = "1.10.2" unifiedpush = "3.0.10" -vico-charts = "2.4.3" +vico-charts-compose = "3.0.3" zelory = "3.0.1" zoomable = "2.11.1" zxing = "3.5.4" @@ -164,10 +164,9 @@ secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jn tarsosdsp = { group = "be.tarsos.dsp", name = "core", version.ref = "tarsosdsp" } tor-android = { module = "info.guardianproject:tor-android", version.ref = "torAndroid" } unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } -vico-charts-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico-charts" } -vico-charts-core = { group = "com.patrykandpatrick.vico", name = "core", version.ref = "vico-charts" } -vico-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico-charts" } -vico-charts-views = { group = "com.patrykandpatrick.vico", name = "views", version.ref = "vico-charts" } +vico-charts-compose = { group = "com.patrykandpatrick.vico", name = "compose", version.ref = "vico-charts-compose" } +vico-charts-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico-charts-compose" } +vico-charts-views = { group = "com.patrykandpatrick.vico", name = "views", version.ref = "vico-charts-compose" } zelory-image-compressor = { group = "id.zelory", name = "compressor", version.ref = "zelory" } zoomable = { group = "net.engawapg.lib", name = "zoomable", version.ref = "zoomable" } zxing = { group = "com.google.zxing", name = "core", version.ref = "zxing" } From 1af1383428f5444a66aeb4c9a84a327a7f4e5648 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 16 Mar 2026 17:09:41 +0000 Subject: [PATCH 139/172] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 5 +++++ amethyst/src/main/res/values-de-rDE/strings.xml | 5 +++++ amethyst/src/main/res/values-hu-rHU/strings.xml | 5 +++++ amethyst/src/main/res/values-pt-rBR/strings.xml | 5 +++++ amethyst/src/main/res/values-sv-rSE/strings.xml | 5 +++++ 5 files changed, 25 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index fa6683bf49..56b1c6a499 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -669,6 +669,7 @@ Zapisovat do Relay Množství bajtů, které bylo odesláno na toto relé, včetně filtrů a událostí Množství bajtů, které bylo přijato z tohoto relé, včetně filtrů a událostí + Uložené události Při pokusu o získání informací z Relay se vyskytla chyba z %1$s Vlastník Používáno @@ -1553,4 +1554,8 @@ %1$d%% dostupnost Nastavení Namecoin Průzkumník Bitcoin (OTS) + události + DMs + profily + nastavení relé diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index cfe786197a..13ba58cc32 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -674,6 +674,7 @@ anz der Bedingungen ist erforderlich In Relay schreiben Die Menge in Bytes, die an dieses Relais gesendet wurde, einschließlich Filter und Ereignisse Die Menge in Bytes, die von diesem Relais empfangen wurde, einschließlich Filter und Ereignisse + Ereignisse gespeichert Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten Inhaber Verwendet von @@ -1558,4 +1559,8 @@ anz der Bedingungen ist erforderlich %1$d%% Verfügbarkeit Namecoin-Einstellungen Bitcoin Explorer (OTS) + ereignisse + DMs + profile + relaiseinstellungen diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 8615b4bfb1..c1b1eed469 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -673,6 +673,7 @@ Írás az átjátszóra Az átjátszónak küldött bájt-mennyiség, beleértve a szűrőket és eseményeket is Az átjátszótól kapott bájt-mennyiség, beleértve a szűrőket és eseményeket is + Tárolt események Hiba lépett fel, amikor megpróbálta lekérni az átjátszó-információt innen: %1$s Tulajdonos Használat a következővel: @@ -1558,4 +1559,8 @@ Üzemidő: %1$d%% Namecoin-beállítások Bitcoin felfedező (OTS) + események + Közvetlen üzenetek + profilok + átjátszóbeállítások diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 3816f9dfbf..003f4da755 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -669,6 +669,7 @@ Enviar para o Relay A quantidade em bytes que foi enviada para este relé, incluindo filtros e eventos A quantidade em bytes que foi recebida deste relé, incluindo filtros e eventos + Eventos armazenados Ocorreu um erro ao tentar obter informações do relay de %1$s Proprietário Usado por @@ -1553,4 +1554,8 @@ %1$d%% de disponibilidade Configurações do Namecoin Explorador Bitcoin (OTS) + eventos + DMs + perfils + configurações de Relay diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index d48eddc078..ad0ee1a94e 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -668,6 +668,7 @@ Skriv till Relay Mängden data i byte som skickades till detta relä, inklusive filter och händelser Mängden data i byte som mottogs från detta relä, inklusive filter och händelser + Lagrade händelser Ett fel inträffade vid försök att hämta information från Relay %1$s Ägare Används av @@ -1552,4 +1553,8 @@ %1$d%% drifttid Namecoin-inställningar Bitcoin Explorer (OTS) + händelser + DMs + profiler + relä inställningar From 7c22c4e6c5c6ff73cccc6a394a513f33fb022ed3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 13:45:01 -0400 Subject: [PATCH 140/172] Migrates to collections 1.6, which deprecates iOS Intel targets. --- .claude/core-skills-plan.md | 2 +- .../references/dependency-graph.md | 3 +-- .claude/skills/kotlin-multiplatform/SKILL.md | 6 +++--- .../references/source-set-hierarchy.md | 6 ++---- .../references/target-compatibility.md | 5 ++--- gradle/libs.versions.toml | 3 +-- quartz/build.gradle.kts | 19 ++----------------- 7 files changed, 12 insertions(+), 32 deletions(-) diff --git a/.claude/core-skills-plan.md b/.claude/core-skills-plan.md index 6b51cdab0a..f85c0d9a18 100644 --- a/.claude/core-skills-plan.md +++ b/.claude/core-skills-plan.md @@ -119,7 +119,7 @@ Create 8 hybrid domain skills combining general expertise with AmethystMultiplat **Focus:** iosMain patterns, Swift/KMP interop, XCFramework generation **SKILL.md sections:** -- iOS source sets: iosMain, iosX64Main, iosArm64Main +- iOS source sets: iosMain, iosArm64Main - Swift interop: type mapping, nullability - expect/actual iOS: 10+ examples from quartz/iosMain - XCFramework setup: baseName = "quartz-kmpKit" diff --git a/.claude/skills/gradle-expert/references/dependency-graph.md b/.claude/skills/gradle-expert/references/dependency-graph.md index 3f38203989..9417851c41 100644 --- a/.claude/skills/gradle-expert/references/dependency-graph.md +++ b/.claude/skills/gradle-expert/references/dependency-graph.md @@ -47,7 +47,7 @@ ### :quartz (KMP Nostr Library) **Type:** Kotlin Multiplatform Library -**Targets:** JVM, Android, iOS (iosX64, iosArm64, iosSimulatorArm64) +**Targets:** JVM, Android, iOS (iosArm64, iosSimulatorArm64) **Dependencies:** - External: secp256k1, jackson, okhttp, kotlinx.coroutines, kotlinx.collections.immutable - Source sets: commonMain → jvmAndroid → {androidMain, jvmMain}, iosMain @@ -127,7 +127,6 @@ commonMain (base) │ ├─ androidMain (Android platform) │ └─ jvmMain (Desktop platform) └─ iosMain (iOS platform) - ├─ iosX64Main ├─ iosArm64Main └─ iosSimulatorArm64Main ``` diff --git a/.claude/skills/kotlin-multiplatform/SKILL.md b/.claude/skills/kotlin-multiplatform/SKILL.md index b0aa2f8d6d..6f8e43f42d 100644 --- a/.claude/skills/kotlin-multiplatform/SKILL.md +++ b/.claude/skills/kotlin-multiplatform/SKILL.md @@ -110,8 +110,8 @@ Think of source sets as a dependency graph, not folders. │ - Jackson │ │ │ │ - OkHttp │ └────┬─────────────┘ └───┬───────────┬───┘ │ - │ │ ├─→ iosX64Main - ▼ ▼ ├─→ iosArm64Main + │ │ │ + ▼ ▼ ├─→ iosArm64Main ┌─────────┐ ┌──────────┐ └─→ iosSimulatorArm64Main │android │ │jvmMain │ │Main │ │(Desktop) │ @@ -252,7 +252,7 @@ expect fun currentTimeSeconds(): Long **iOS (iosMain):** - Active development, framework configured -- Architecture targets: iosX64Main, iosArm64Main, iosSimulatorArm64Main +- Architecture targets: macosArm64Main, iosArm64Main, iosSimulatorArm64Main - Platform APIs via platform.posix, Security framework ### Web, wasm - Future Targets diff --git a/.claude/skills/kotlin-multiplatform/references/source-set-hierarchy.md b/.claude/skills/kotlin-multiplatform/references/source-set-hierarchy.md index ec09e00332..b950c57d5b 100644 --- a/.claude/skills/kotlin-multiplatform/references/source-set-hierarchy.md +++ b/.claude/skills/kotlin-multiplatform/references/source-set-hierarchy.md @@ -29,7 +29,7 @@ Visual guide to source set organization with concrete examples from the codebase │ - Jackson │ │ - Platform libs │ │ - OkHttp │ └───────┬───────────┘ └────┬─────────┬───┘ │ - │ │ ├─→ iosX64Main (simulator Intel) + │ │ │ │ │ ├─→ iosArm64Main (device ARM64) │ │ └─→ iosSimulatorArm64Main (Apple Silicon) ▼ ▼ @@ -238,7 +238,6 @@ iosMain { } } -val iosX64Main by getting { dependsOn(iosMain.get()) } val iosArm64Main by getting { dependsOn(iosMain.get()) } val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) } ``` @@ -249,7 +248,6 @@ val iosSimulatorArm64Main by getting { dependsOn(iosMain.get()) } - Different from Android/Desktop **Architecture targets:** -- iosX64Main: Intel simulator - iosArm64Main: Device (iPhone, iPad) - iosSimulatorArm64Main: Apple Silicon simulator @@ -326,7 +324,7 @@ commonMain | androidMain | jvmAndroid | Android framework | Activity, ViewModel | | jvmMain | jvmAndroid | JVM + Compose Desktop | Window, MenuBar | | iosMain | commonMain | iOS platform | Security framework | -| iosX64Main | iosMain | Simulator (Intel) | Architecture-specific | +| iosMain | Simulator (Intel) | Architecture-specific | | iosArm64Main | iosMain | Device (ARM64) | Architecture-specific | | jsMain | commonMain | JS/DOM | Web (future) | | wasmMain | commonMain | wasm APIs | WebAssembly (future) | diff --git a/.claude/skills/kotlin-multiplatform/references/target-compatibility.md b/.claude/skills/kotlin-multiplatform/references/target-compatibility.md index 7ccf720fb8..c2a4d71299 100644 --- a/.claude/skills/kotlin-multiplatform/references/target-compatibility.md +++ b/.claude/skills/kotlin-multiplatform/references/target-compatibility.md @@ -76,7 +76,6 @@ fun main() = application { **Source sets:** - iosMain (common iOS code) -- iosX64Main (Intel simulator) - iosArm64Main (device - iPhone/iPad) - iosSimulatorArm64Main (Apple Silicon simulator) @@ -110,7 +109,7 @@ actual object Secp256k1Instance { ```kotlin // quartz/build.gradle.kts kotlin { - listOf(iosX64(), iosArm64(), iosSimulatorArm64()) + listOf(macosArm64(), iosArm64(), iosSimulatorArm64()) .forEach { target -> target.binaries.framework { baseName = "quartz-kmpKit" @@ -310,7 +309,7 @@ fun parseJson(json: String): Event { - Manual desktop app testing **iOS:** -- Unit tests: iosTest (iosX64Test, iosArm64Test, etc.) +- Unit tests: iosTest (iosArm64Test, etc.) - Simulator/device testing **Web (future):** diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a9963706c7..9fda673e2e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ zxing = "3.5.4" zxingAndroidEmbedded = "4.3.0" windowCoreAndroid = "1.5.1" androidxCamera = "1.5.3" -androidxCollection = "1.5.0" +androidxCollection = "1.6.0" kotlinTest = "2.3.0" core = "1.7.0" mavenPublish = "0.36.0" @@ -103,7 +103,6 @@ androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3- androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } androidx-media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" } androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" } -androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } androidx-media3-ui-compose-material3 = { group = "androidx.media3", name = "media3-ui-compose-material3", version.ref = "media3" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index a2dc608f34..7912bcf70c 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -83,12 +83,11 @@ kotlin { listOf( iosArm64(), - iosX64(), iosSimulatorArm64(), ).forEach { target -> target.compilations.getByName("main") { - val Clibsodium by cinterops.creating { + val clibsodium by cinterops.creating { definitionFile = libsodiumDefFile packageName = "Clibsodium" @@ -99,7 +98,7 @@ kotlin { ) } - tasks.named(cinterops.getByName("Clibsodium").interopProcessingTaskName).configure { + tasks.named(cinterops.getByName("clibsodium").interopProcessingTaskName).configure { dependsOn(libsodiumDefFileGeneration) } } @@ -120,12 +119,6 @@ kotlin { } } - iosX64 { - binaries.framework { - baseName = xcfName - } - } - iosArm64 { binaries.framework { baseName = xcfName @@ -281,10 +274,6 @@ kotlin { } } - val iosX64Main by getting { - dependsOn(iosMain.get()) - } - val iosArm64Main by getting { dependsOn(iosMain.get()) } @@ -299,10 +288,6 @@ kotlin { } } - val iosX64Test by getting { - dependsOn(iosTest.get()) - } - val iosArm64Test by getting { dependsOn(iosTest.get()) } From 21b6f9c67e3b95da8898cd4dfdeaa945150afeeb Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 16 Mar 2026 17:50:19 +0000 Subject: [PATCH 141/172] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-pl-rPL/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index a10363d70b..dceddba9fa 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -670,6 +670,7 @@ Zapisz do Transmitera Ilość w bajtach, która została wysłana do tego transmitera, w tym filtry i wydarzenia Ilość w bajtach, która została otrzymana z tego transmitera, w tym filtry i wydarzenia + Zapisane zdarzenia Wystąpił błąd podczas próby uzyskania informacji o transmiterze z %1$s Operator Używany przez @@ -925,6 +926,8 @@ Upewnij się, że aplikacja logującego autoryzuje tę operację Nie znaleziono portfeli do zapłacenia faktury z Lightning (Error: %1$s). Proszę zainstalować Lightning wallet, aby używać zapów Nie znaleziono portfeli do zapłacenia faktury z Lightning. Proszę zainstalować Lightning wallet, aby używać zapów + Nie można otworzyć linków Blossom + Nie znaleziono aplikacji Blossom. Zainstaluj lokalną aplikację Blossom, aby wyświetlić ten plik Ukryte słowa Ukryj nowe słowo lub wyrażenie Zdjęcie profilowe @@ -1047,6 +1050,7 @@ Wszystkie Filmiki Szachy + Portfel Filtry bezpieczeństwa Importuj Obserwujących Nowy post From bf6ee6dc8708c67cfdab8567677d7db38e6995fb Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 16 Mar 2026 19:01:51 +0100 Subject: [PATCH 142/172] Split the logic between onPreScroll and onPostScroll --- .../ui/components/ZonedSwipeModifier.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt index 2579251446..b72c343154 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ZonedSwipeModifier.kt @@ -60,12 +60,13 @@ fun Modifier.zonedDrawerSwipe( if (source != NestedScrollSource.UserInput) return Offset.Zero if (drawerOpened) return Offset(available.x, 0f) - // available.x > 0 means user is swiping right + // Non-first pages in the drawer zone: intercept before the + // pager consumes the delta to page backwards. if (available.x > 0f) { val wasOnFirstPage = gestureStartPage == 0 val isInPagerZone = gestureStartX < widthPx * PAGER_ZONE_FRACTION - if (wasOnFirstPage || !isInPagerZone) { + if (!wasOnFirstPage && !isInPagerZone) { drawerOpened = true openDrawer() return Offset(available.x, 0f) @@ -73,6 +74,24 @@ fun Modifier.zonedDrawerSwipe( } return Offset.Zero } + + override fun onPostScroll( + consumed: Offset, + available: Offset, + source: NestedScrollSource, + ): Offset { + if (source != NestedScrollSource.UserInput) return Offset.Zero + if (drawerOpened) return Offset(available.x, 0f) + + // First page: open drawer only with unconsumed right-swipe + // so child LazyRows can scroll first. + if (available.x > 0f && gestureStartPage == 0) { + drawerOpened = true + openDrawer() + return Offset(available.x, 0f) + } + return Offset.Zero + } } } From 9d95817b4d0cfd589fa7c2d7b4dada7ebeec5fad Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 16 Mar 2026 19:04:01 +0100 Subject: [PATCH 143/172] drawerGesturesEnabled is true whenever the drawer is in transition (target != current) --- .../vitorpamplona/amethyst/ui/navigation/AppNavigation.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index d91803756a..a613eac3c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -149,7 +149,10 @@ fun AppNavigation( navBackStackEntry?.destination?.let { dest -> dest.hasRoute() || dest.hasRoute() } ?: false - val drawerGesturesEnabled = !isTabPagerRoute || nav.drawerState.isOpen + val drawerGesturesEnabled = + !isTabPagerRoute || + nav.drawerState.isOpen || + nav.drawerState.targetValue != nav.drawerState.currentValue AccountSwitcherAndLeftDrawerLayout(accountViewModel, accountSessionManager, nav, drawerGesturesEnabled) { NavHost( From 1a671fd0b480038b5d4a7b4143be3956d252b22f Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 16 Mar 2026 18:27:11 +0000 Subject: [PATCH 144/172] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index dceddba9fa..3e737905ba 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -1051,6 +1051,28 @@ Filmiki Szachy Portfel + Saldo + Wyślij + Odbierz + Transakcje + Nie podłączono portfela + Aby korzystać z portfela, skonfiguruj połączenie Nostr Wallet Connect (NWC) w ustawieniach zap. + Skonfiguruj portfel + satosze + Wstaw fakturę BOLT-11 + Zapłać + Płatność udana + Wysyłanie zapłaty… + Kwota (satoszy) + Opis (opcjonalnie) + Utwórz fakturę + Tworzenie faktury… + Kopiuj fakturę + Brak dostępnych transakcji + Wczytywanie… + Otrzymano + Wysłano + Odśwież Filtry bezpieczeństwa Importuj Obserwujących Nowy post @@ -1286,6 +1308,7 @@ Wybierz język, na który chcesz przetłumaczyć treść. Ustawienia językowe Dla każdej pary językowej wybierz, który język ma być wyświetlany jako pierwszy. + %1$s - %2$s Wyszukiwanie Języków Dodaj język Dodaj pary językowe @@ -1303,6 +1326,7 @@ Znaleziono raport o błędzie Czy chcesz wysłać ostatni raport o awarii do Amethyst w DM? Żadne dane osobowe nie będą udostępnione Prześlij + Ta wiadomość zniknie za %1$s Ta wiadomość zniknie za %1$d dni Wybierz Sygnatariusza Już jest na liście From abf2151bac43e9aaf5860e8c2340b7b19da050d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 16:25:16 -0400 Subject: [PATCH 145/172] Damus' relay is way too restrictive --- .../quartz/nip01Core/relay/NostrClientManualSubTest.kt | 2 +- .../quartz/nip01Core/relay/NostrClientRepeatSubTest.kt | 6 +++--- .../nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt | 2 +- .../quartz/nip01Core/relay/NostrClientSubscriptionTest.kt | 2 +- .../relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt index 338a832e8b..e974d964da 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientManualSubTest.kt @@ -70,7 +70,7 @@ class NostrClientManualSubTest : BaseNostrClientTest() { val filters = mapOf( - RelayUrlNormalizer.normalize("wss://relay.damus.io") to + RelayUrlNormalizer.normalize("wss://nos.lol") to listOf( Filter( kinds = listOf(MetadataEvent.KIND), diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientRepeatSubTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientRepeatSubTest.kt index e583343c50..7334cb1583 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientRepeatSubTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientRepeatSubTest.kt @@ -83,7 +83,7 @@ class NostrClientRepeatSubTest : BaseNostrClientTest() { val filters = mapOf( - RelayUrlNormalizer.normalize("wss://relay.damus.io") to + RelayUrlNormalizer.normalize("wss://nos.lol") to listOf( Filter( kinds = listOf(MetadataEvent.KIND), @@ -94,7 +94,7 @@ class NostrClientRepeatSubTest : BaseNostrClientTest() { val filtersShouldIgnore = mapOf( - RelayUrlNormalizer.normalize("wss://relay.damus.io") to + RelayUrlNormalizer.normalize("wss://nos.lol") to listOf( Filter( kinds = listOf(AdvertisedRelayListEvent.KIND), @@ -105,7 +105,7 @@ class NostrClientRepeatSubTest : BaseNostrClientTest() { val filtersShouldSendAfterEOSE = mapOf( - RelayUrlNormalizer.normalize("wss://relay.damus.io") to + RelayUrlNormalizer.normalize("wss://nos.lol") to listOf( Filter( kinds = listOf(AdvertisedRelayListEvent.KIND), diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt index e28b887f2b..7afd9caad4 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionAsFlowTest.kt @@ -93,7 +93,7 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() { val flow = client.reqAsFlow( - relay = "wss://relay.damus.io", + relay = "wss://nos.lol", filter = Filter( kinds = listOf(MetadataEvent.KIND), diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionTest.kt index 511a0d2a5b..00c37f00ea 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionTest.kt @@ -48,7 +48,7 @@ class NostrClientSubscriptionTest : BaseNostrClientTest() { val sub = client.req( - relay = "wss://relay.damus.io", + relay = "wss://nos.lol", filter = Filter( kinds = listOf(MetadataEvent.KIND), diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt index 913bf5eff9..de36239681 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/relay/NostrClientSubscriptionUntilEoseAsFlowTest.kt @@ -93,7 +93,7 @@ class NostrClientSubscriptionUntilEoseAsFlowTest : BaseNostrClientTest() { val flow = client.reqUntilEoseAsFlow( - relay = "wss://relay.damus.io", + relay = "wss://nos.lol", filter = Filter( kinds = listOf(MetadataEvent.KIND), From b916817247c433b83cc3adc331306412d36badda Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 16:35:05 -0400 Subject: [PATCH 146/172] Finishes serialization features for NIP-47 --- quartz/build.gradle.kts | 5 + .../KotlinSerializationMapper.kt | 12 ++ .../nip47WalletConnect/NwcTransaction.kt | 2 +- .../quartz/nip47WalletConnect/Request.kt | 8 +- .../quartz/nip47WalletConnect/Response.kt | 2 +- .../kotlinSerialization/JsonExt.kt | 48 +++++ .../Nip47NotificationKSerializer.kt | 42 ++++- .../Nip47RequestKSerializer.kt | 165 +++++++++++++++- .../Nip47ResponseKSerializer.kt | 178 +++++++++++++++++- .../quartz/nip01Core/jackson/JacksonMapper.kt | 6 + .../jackson/NotificationSerializer.kt | 60 ++++++ .../jackson/RequestSerializer.kt | 111 +++++++++++ .../jackson/ResponseSerializer.kt | 146 ++++++++++++++ 13 files changed, 776 insertions(+), 9 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/JsonExt.kt create mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/NotificationSerializer.kt create mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestSerializer.kt create mode 100644 quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseSerializer.kt diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index 7912bcf70c..b9ffc2add8 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -247,9 +247,14 @@ kotlin { } getByName("androidHostTest") { + dependsOn(jvmAndroidTest) dependencies { // Bitcoin secp256k1 bindings implementation(libs.secp256k1.kmp.jni.jvm) + + // LibSodium for ChaCha encryption (NIP-44) - Needed for host tests + implementation(libs.lazysodium.java) + implementation(libs.jna) } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt index d5b79dbf47..d0f81a3bcf 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/kotlinSerialization/KotlinSerializationMapper.kt @@ -107,6 +107,18 @@ class KotlinSerializationMapper { json.encodeToString(BunkerMessageKSerializer, value) } + is Request -> { + json.encodeToString(Nip47RequestKSerializer, value) + } + + is Response -> { + json.encodeToString(Nip47ResponseKSerializer, value) + } + + is Notification -> { + json.encodeToString(Nip47NotificationKSerializer, value) + } + else -> { throw IllegalArgumentException("Unsupported type: ${value::class}") } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt index adff64b56c..c0db67e710 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/NwcTransaction.kt @@ -62,7 +62,7 @@ class NwcTransaction( var expires_at: Long? = null, var settled_at: Long? = null, var settle_deadline: Long? = null, - var metadata: Any? = null, + var metadata: Map? = null, ) { fun parsedMetadata(): NwcTransactionMetadata? = NwcTransactionMetadata.parse(metadata) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt index 26e5711a3e..fe00ca44c5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Request.kt @@ -31,7 +31,7 @@ abstract class Request( class PayInvoiceParams( var invoice: String? = null, var amount: Long? = null, - var metadata: Any? = null, + var metadata: Map? = null, ) class PayInvoiceMethod( @@ -74,7 +74,7 @@ class MakeInvoiceParams( var description: String? = null, var description_hash: String? = null, var expiry: Long? = null, - var metadata: Any? = null, + var metadata: Map? = null, ) class MakeInvoiceMethod( @@ -233,7 +233,7 @@ class CreateConnectionParams( var budget_renewal: String? = null, var expires_at: Long? = null, var isolated: Boolean? = null, - var metadata: Any? = null, + var metadata: Map? = null, ) class CreateConnectionMethod( @@ -249,7 +249,7 @@ class CreateConnectionMethod( budgetRenewal: String? = null, expiresAt: Long? = null, isolated: Boolean? = null, - metadata: Any? = null, + metadata: Map? = null, ): CreateConnectionMethod = CreateConnectionMethod( CreateConnectionParams(pubkey, name, requestMethods, notificationTypes, maxAmount, budgetRenewal, expiresAt, isolated, metadata), diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt index 623f0cee6a..6547443ba8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt @@ -105,7 +105,7 @@ class GetInfoSuccessResponse( val block_hash: String? = null, val methods: List? = null, val notifications: List? = null, - val metadata: Any? = null, + val metadata: Map? = null, val lud16: String? = null, ) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/JsonExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/JsonExt.kt new file mode 100644 index 0000000000..472972cb6e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/JsonExt.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect.kotlinSerialization + +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive + +// Helper function to convert JsonElement to standard Kotlin types recursively +fun JsonElement.toAnyValue(): Any = + when (this) { + is JsonPrimitive -> { + if (isString) { + content + } else { + content.toBooleanStrictOrNull() ?: content.toDoubleOrNull() ?: content.toLongOrNull() ?: content + } + } + + is JsonObject -> { + toAnyMap() + } + + is JsonArray -> { + map { it.toAnyValue() } + } + } + +fun JsonObject.toAnyMap(): Map = entries.associate { it.key to it.value.toAnyValue() } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt index 811e46fe44..715bd6beec 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47NotificationKSerializer.kt @@ -32,10 +32,13 @@ import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.put object Nip47NotificationKSerializer : KSerializer { override val descriptor: SerialDescriptor = @@ -44,7 +47,44 @@ object Nip47NotificationKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Notification, - ): Unit = throw UnsupportedOperationException("NIP-47 Notification serialization not supported") + ) { + val jsonEncoder = encoder as JsonEncoder + val jsonObject = + buildJsonObject { + put("notification_type", value.notification_type) + when (value) { + is PaymentReceivedNotification -> { + Nip47ResponseKSerializer.serializeTransaction(value.notification)?.let { + put("notification", it) + } + } + + is PaymentSentNotification -> { + Nip47ResponseKSerializer.serializeTransaction(value.notification)?.let { + put("notification", it) + } + } + + is HoldInvoiceAcceptedNotification -> { + value.notification?.let { data -> + put( + "notification", + buildJsonObject { + data.type?.let { put("type", it) } + data.invoice?.let { put("invoice", it) } + data.payment_hash?.let { put("payment_hash", it) } + data.amount?.let { put("amount", it) } + data.created_at?.let { put("created_at", it) } + data.expires_at?.let { put("expires_at", it) } + data.settle_deadline?.let { put("settle_deadline", it) } + }, + ) + } + } + } + } + jsonEncoder.encodeJsonElement(jsonObject) + } override fun deserialize(decoder: Decoder): Notification { val jsonDecoder = decoder as JsonDecoder diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt index 691627409c..87e7e0a0ff 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47RequestKSerializer.kt @@ -51,15 +51,23 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonNull.content import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.put object Nip47RequestKSerializer : KSerializer { override val descriptor: SerialDescriptor = @@ -68,7 +76,159 @@ object Nip47RequestKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Request, - ): Unit = throw UnsupportedOperationException("NIP-47 Request serialization not supported") + ) { + val jsonEncoder = encoder as JsonEncoder + val jsonObject = + buildJsonObject { + put("method", value.method) + when (value) { + is PayInvoiceMethod -> { + value.params?.let { put("params", serializePayInvoiceParams(it)) } + } + + is PayKeysendMethod -> { + value.params?.let { put("params", serializePayKeysendParams(it)) } + } + + is MakeInvoiceMethod -> { + value.params?.let { put("params", serializeMakeInvoiceParams(it)) } + } + + is LookupInvoiceMethod -> { + value.params?.let { put("params", serializeLookupInvoiceParams(it)) } + } + + is ListTransactionsMethod -> { + value.params?.let { put("params", serializeListTransactionsParams(it)) } + } + + is GetBalanceMethod -> {} + + is GetInfoMethod -> {} + + is GetBudgetMethod -> {} + + is SignMessageMethod -> { + value.params?.let { put("params", serializeSignMessageParams(it)) } + } + + is CreateConnectionMethod -> { + value.params?.let { put("params", serializeCreateConnectionParams(it)) } + } + + is MakeHoldInvoiceMethod -> { + value.params?.let { put("params", serializeMakeHoldInvoiceParams(it)) } + } + + is CancelHoldInvoiceMethod -> { + value.params?.let { put("params", serializeCancelHoldInvoiceParams(it)) } + } + + is SettleHoldInvoiceMethod -> { + value.params?.let { put("params", serializeSettleHoldInvoiceParams(it)) } + } + } + } + jsonEncoder.encodeJsonElement(jsonObject) + } + + private fun serializePayInvoiceParams(params: PayInvoiceParams): JsonObject = + buildJsonObject { + params.invoice?.let { put("invoice", it) } + params.amount?.let { put("amount", it) } + params.metadata?.let { put("metadata", Json.encodeToJsonElement(it)) } + } + + private fun serializePayKeysendParams(params: PayKeysendParams): JsonObject = + buildJsonObject { + params.amount?.let { put("amount", it) } + params.pubkey?.let { put("pubkey", it) } + params.preimage?.let { put("preimage", it) } + params.tlv_records?.let { records -> + put( + "tlv_records", + buildJsonArray { + records.forEach { record -> + add( + buildJsonObject { + record.type?.let { put("type", it) } + record.value?.let { put("value", it) } + }, + ) + } + }, + ) + } + } + + private fun serializeMakeInvoiceParams(params: MakeInvoiceParams): JsonObject = + buildJsonObject { + params.amount?.let { put("amount", it) } + params.description?.let { put("description", it) } + params.description_hash?.let { put("description_hash", it) } + params.expiry?.let { put("expiry", it) } + params.metadata?.let { put("metadata", Json.encodeToJsonElement(it)) } + } + + private fun serializeLookupInvoiceParams(params: LookupInvoiceParams): JsonObject = + buildJsonObject { + params.payment_hash?.let { put("payment_hash", it) } + params.invoice?.let { put("invoice", it) } + } + + private fun serializeListTransactionsParams(params: ListTransactionsParams): JsonObject = + buildJsonObject { + params.from?.let { put("from", it) } + params.until?.let { put("until", it) } + params.limit?.let { put("limit", it) } + params.offset?.let { put("offset", it) } + params.unpaid?.let { put("unpaid", it) } + params.unpaid_outgoing?.let { put("unpaid_outgoing", it) } + params.unpaid_incoming?.let { put("unpaid_incoming", it) } + params.type?.let { put("type", it) } + } + + private fun serializeSignMessageParams(params: SignMessageParams): JsonObject = + buildJsonObject { + params.message?.let { put("message", it) } + } + + private fun serializeCreateConnectionParams(params: CreateConnectionParams): JsonObject = + buildJsonObject { + params.pubkey?.let { put("pubkey", it) } + params.name?.let { put("name", it) } + params.request_methods?.let { methods -> + put("request_methods", buildJsonArray { methods.forEach { add(it) } }) + } + params.notification_types?.let { types -> + put("notification_types", buildJsonArray { types.forEach { add(it) } }) + } + params.max_amount?.let { put("max_amount", it) } + params.budget_renewal?.let { put("budget_renewal", it) } + params.expires_at?.let { put("expires_at", it) } + params.isolated?.let { put("isolated", it) } + params.metadata?.let { put("metadata", Json.encodeToJsonElement(it)) } + } + + private fun serializeMakeHoldInvoiceParams(params: MakeHoldInvoiceParams): JsonObject = + buildJsonObject { + params.amount?.let { put("amount", it) } + params.description?.let { put("description", it) } + params.description_hash?.let { put("description_hash", it) } + params.expiry?.let { put("expiry", it) } + params.payment_hash?.let { put("payment_hash", it) } + params.min_cltv_expiry_delta?.let { put("min_cltv_expiry_delta", it) } + } + + private fun serializeCancelHoldInvoiceParams(params: CancelHoldInvoiceParams): JsonObject = + buildJsonObject { + params.payment_hash?.let { put("payment_hash", it) } + } + + private fun serializeSettleHoldInvoiceParams(params: SettleHoldInvoiceParams): JsonObject = + buildJsonObject { + params.preimage?.let { put("preimage", it) } + } override fun deserialize(decoder: Decoder): Request { val jsonDecoder = decoder as JsonDecoder @@ -100,6 +260,7 @@ object Nip47RequestKSerializer : KSerializer { PayInvoiceParams( invoice = it["invoice"]?.jsonPrimitive?.content, amount = it["amount"]?.jsonPrimitive?.longOrNull, + metadata = it["metadata"]?.jsonObject?.toAnyMap(), ) }, ) @@ -135,6 +296,7 @@ object Nip47RequestKSerializer : KSerializer { description = it["description"]?.jsonPrimitive?.content, description_hash = it["description_hash"]?.jsonPrimitive?.content, expiry = it["expiry"]?.jsonPrimitive?.longOrNull, + metadata = it["metadata"]?.jsonObject?.toAnyMap(), ) }, ) @@ -194,6 +356,7 @@ object Nip47RequestKSerializer : KSerializer { budget_renewal = it["budget_renewal"]?.jsonPrimitive?.content, expires_at = it["expires_at"]?.jsonPrimitive?.longOrNull, isolated = it["isolated"]?.jsonPrimitive?.booleanOrNull, + metadata = it["metadata"]?.jsonObject?.toAnyMap(), ) }, ) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt index 7c124de3cd..e7b9bed88e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/kotlinSerialization/Nip47ResponseKSerializer.kt @@ -45,13 +45,20 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonEncoder import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.add +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.longOrNull +import kotlinx.serialization.json.put object Nip47ResponseKSerializer : KSerializer { override val descriptor: SerialDescriptor = @@ -60,7 +67,154 @@ object Nip47ResponseKSerializer : KSerializer { override fun serialize( encoder: Encoder, value: Response, - ): Unit = throw UnsupportedOperationException("NIP-47 Response serialization not supported") + ) { + val jsonEncoder = encoder as JsonEncoder + val jsonObject = + buildJsonObject { + put("result_type", value.resultType) + when (value) { + is NwcErrorResponse -> { + value.error?.let { put("error", serializeNwcError(it)) } + } + + is PayInvoiceSuccessResponse -> { + value.result?.let { put("result", serializePayInvoiceResult(it)) } + } + + is PayInvoiceErrorResponse -> { + value.error?.let { put("error", serializePayInvoiceErrorParams(it)) } + } + + is PayKeysendSuccessResponse -> { + value.result?.let { put("result", serializePayKeysendResult(it)) } + } + + is MakeInvoiceSuccessResponse -> { + serializeTransaction(value.result)?.let { put("result", it) } + } + + is LookupInvoiceSuccessResponse -> { + serializeTransaction(value.result)?.let { put("result", it) } + } + + is ListTransactionsSuccessResponse -> { + value.result?.let { put("result", serializeListTransactionsResult(it)) } + } + + is GetBalanceSuccessResponse -> { + value.result?.let { put("result", serializeGetBalanceResult(it)) } + } + + is GetInfoSuccessResponse -> { + value.result?.let { put("result", serializeGetInfoResult(it)) } + } + + is GetBudgetSuccessResponse -> { + value.result?.let { put("result", serializeGetBudgetResult(it)) } + } + + is SignMessageSuccessResponse -> { + value.result?.let { put("result", serializeSignMessageResult(it)) } + } + + is CreateConnectionSuccessResponse -> { + value.result?.let { put("result", serializeCreateConnectionResult(it)) } + } + + is MakeHoldInvoiceSuccessResponse -> { + serializeTransaction(value.result)?.let { put("result", it) } + } + + is CancelHoldInvoiceSuccessResponse -> { + put("result", buildJsonObject {}) + } + + is SettleHoldInvoiceSuccessResponse -> { + put("result", buildJsonObject {}) + } + } + } + jsonEncoder.encodeJsonElement(jsonObject) + } + + private fun serializeNwcError(error: NwcError): JsonObject = + buildJsonObject { + error.code?.let { put("code", it.name) } + error.message?.let { put("message", it) } + } + + private fun serializePayInvoiceResult(result: PayInvoiceSuccessResponse.PayInvoiceResultParams): JsonObject = + buildJsonObject { + result.preimage?.let { put("preimage", it) } + result.fees_paid?.let { put("fees_paid", it) } + } + + private fun serializePayInvoiceErrorParams(error: PayInvoiceErrorResponse.PayInvoiceErrorParams): JsonObject = + buildJsonObject { + error.code?.let { put("code", it.name) } + error.message?.let { put("message", it) } + } + + private fun serializePayKeysendResult(result: PayKeysendSuccessResponse.PayKeysendResult): JsonObject = + buildJsonObject { + result.preimage?.let { put("preimage", it) } + result.fees_paid?.let { put("fees_paid", it) } + } + + private fun serializeListTransactionsResult(result: ListTransactionsSuccessResponse.ListTransactionsResult): JsonObject = + buildJsonObject { + result.transactions?.let { transactions -> + put( + "transactions", + buildJsonArray { + transactions.forEach { serializeTransaction(it)?.let { t -> add(t) } } + }, + ) + } + result.total_count?.let { put("total_count", it) } + } + + private fun serializeGetBalanceResult(result: GetBalanceSuccessResponse.GetBalanceResult): JsonObject = + buildJsonObject { + result.balance?.let { put("balance", it) } + } + + private fun serializeGetInfoResult(result: GetInfoSuccessResponse.GetInfoResult): JsonObject = + buildJsonObject { + result.alias?.let { put("alias", it) } + result.color?.let { put("color", it) } + result.pubkey?.let { put("pubkey", it) } + result.network?.let { put("network", it) } + result.block_height?.let { put("block_height", it) } + result.block_hash?.let { put("block_hash", it) } + result.methods?.let { methods -> + put("methods", buildJsonArray { methods.forEach { add(it) } }) + } + result.notifications?.let { notifications -> + put("notifications", buildJsonArray { notifications.forEach { add(it) } }) + } + result.metadata?.let { put("metadata", Json.encodeToJsonElement(it)) } + result.lud16?.let { put("lud16", it) } + } + + private fun serializeGetBudgetResult(result: GetBudgetSuccessResponse.GetBudgetResult): JsonObject = + buildJsonObject { + result.used_budget?.let { put("used_budget", it) } + result.total_budget?.let { put("total_budget", it) } + result.renews_at?.let { put("renews_at", it) } + result.renewal_period?.let { put("renewal_period", it) } + } + + private fun serializeSignMessageResult(result: SignMessageSuccessResponse.SignMessageResult): JsonObject = + buildJsonObject { + result.message?.let { put("message", it) } + result.signature?.let { put("signature", it) } + } + + private fun serializeCreateConnectionResult(result: CreateConnectionSuccessResponse.CreateConnectionResult): JsonObject = + buildJsonObject { + result.wallet_pubkey?.let { put("wallet_pubkey", it) } + } override fun deserialize(decoder: Decoder): Response { val jsonDecoder = decoder as JsonDecoder @@ -162,6 +316,26 @@ object Nip47ResponseKSerializer : KSerializer { return NwcError(code, obj["message"]?.jsonPrimitive?.content) } + fun serializeTransaction(transaction: NwcTransaction?): JsonObject? { + if (transaction == null) return null + return buildJsonObject { + transaction.type?.let { put("type", it) } + transaction.state?.let { put("state", it) } + transaction.invoice?.let { put("invoice", it) } + transaction.description?.let { put("description", it) } + transaction.description_hash?.let { put("description_hash", it) } + transaction.preimage?.let { put("preimage", it) } + transaction.payment_hash?.let { put("payment_hash", it) } + transaction.amount?.let { put("amount", it) } + transaction.fees_paid?.let { put("fees_paid", it) } + transaction.created_at?.let { put("created_at", it) } + transaction.expires_at?.let { put("expires_at", it) } + transaction.settled_at?.let { put("settled_at", it) } + transaction.settle_deadline?.let { put("settle_deadline", it) } + transaction.metadata?.let { put("metadata", Json.encodeToJsonElement(it)) } + } + } + fun parseTransaction(obj: JsonObject?): NwcTransaction? { if (obj == null) return null return NwcTransaction( @@ -178,6 +352,7 @@ object Nip47ResponseKSerializer : KSerializer { expires_at = obj["expires_at"]?.jsonPrimitive?.longOrNull, settled_at = obj["settled_at"]?.jsonPrimitive?.longOrNull, settle_deadline = obj["settle_deadline"]?.jsonPrimitive?.longOrNull, + metadata = obj["metadata"]?.jsonObject?.toAnyMap(), ) } @@ -260,6 +435,7 @@ object Nip47ResponseKSerializer : KSerializer { block_hash = it["block_hash"]?.jsonPrimitive?.content, methods = it["methods"]?.jsonArray?.map { m -> m.jsonPrimitive.content }, notifications = it["notifications"]?.jsonArray?.map { n -> n.jsonPrimitive.content }, + metadata = it["metadata"]?.jsonObject?.toAnyMap(), lud16 = it["lud16"]?.jsonPrimitive?.content, ) }, diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt index 7729b4dd86..34757f17cd 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt @@ -56,8 +56,11 @@ import com.vitorpamplona.quartz.nip47WalletConnect.Notification import com.vitorpamplona.quartz.nip47WalletConnect.Request import com.vitorpamplona.quartz.nip47WalletConnect.Response import com.vitorpamplona.quartz.nip47WalletConnect.jackson.NotificationDeserializer +import com.vitorpamplona.quartz.nip47WalletConnect.jackson.NotificationSerializer import com.vitorpamplona.quartz.nip47WalletConnect.jackson.RequestDeserializer +import com.vitorpamplona.quartz.nip47WalletConnect.jackson.RequestSerializer import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseDeserializer +import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseSerializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorDeserializer import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorSerializer @@ -93,8 +96,11 @@ class JacksonMapper { .addSerializer(Rumor::class.java, RumorSerializer()) .addDeserializer(Rumor::class.java, RumorDeserializer()) // nip 47 + .addSerializer(Response::class.java, ResponseSerializer()) .addDeserializer(Response::class.java, ResponseDeserializer()) + .addSerializer(Request::class.java, RequestSerializer()) .addDeserializer(Request::class.java, RequestDeserializer()) + .addSerializer(Notification::class.java, NotificationSerializer()) .addDeserializer(Notification::class.java, NotificationDeserializer()) // nip 46 .addDeserializer(BunkerMessage::class.java, BunkerMessageDeserializer()) diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/NotificationSerializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/NotificationSerializer.kt new file mode 100644 index 0000000000..591197df42 --- /dev/null +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/NotificationSerializer.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect.jackson + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.vitorpamplona.quartz.nip47WalletConnect.HoldInvoiceAcceptedNotification +import com.vitorpamplona.quartz.nip47WalletConnect.Notification +import com.vitorpamplona.quartz.nip47WalletConnect.PaymentReceivedNotification +import com.vitorpamplona.quartz.nip47WalletConnect.PaymentSentNotification + +class NotificationSerializer : StdSerializer(Notification::class.java) { + override fun serialize( + value: Notification, + gen: JsonGenerator, + provider: SerializerProvider, + ) { + gen.writeStartObject() + gen.writeStringField("notification_type", value.notification_type) + when (value) { + is PaymentReceivedNotification -> { + if (value.notification != null) { + gen.writeObjectField("notification", value.notification) + } + } + + is PaymentSentNotification -> { + if (value.notification != null) { + gen.writeObjectField("notification", value.notification) + } + } + + is HoldInvoiceAcceptedNotification -> { + if (value.notification != null) { + gen.writeObjectField("notification", value.notification) + } + } + } + gen.writeEndObject() + } +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestSerializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestSerializer.kt new file mode 100644 index 0000000000..ac34c82a7a --- /dev/null +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/RequestSerializer.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect.jackson + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionMethod +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsMethod +import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendMethod +import com.vitorpamplona.quartz.nip47WalletConnect.Request +import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceMethod +import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageMethod + +class RequestSerializer : StdSerializer(Request::class.java) { + override fun serialize( + value: Request, + gen: JsonGenerator, + provider: SerializerProvider, + ) { + gen.writeStartObject() + if (value.method != null) { + gen.writeStringField("method", value.method) + } + when (value) { + is PayInvoiceMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is PayKeysendMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is MakeInvoiceMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is LookupInvoiceMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is ListTransactionsMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is MakeHoldInvoiceMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is CancelHoldInvoiceMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is SettleHoldInvoiceMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is SignMessageMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + + is CreateConnectionMethod -> { + if (value.params != null) { + gen.writeObjectField("params", value.params) + } + } + } + gen.writeEndObject() + } +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseSerializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseSerializer.kt new file mode 100644 index 0000000000..933025b638 --- /dev/null +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/jackson/ResponseSerializer.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect.jackson + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.vitorpamplona.quartz.nip47WalletConnect.CancelHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.CreateConnectionSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetBalanceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetBudgetSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.GetInfoSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.ListTransactionsSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.LookupInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.MakeHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.MakeInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.NwcErrorResponse +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.PayKeysendSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.Response +import com.vitorpamplona.quartz.nip47WalletConnect.SettleHoldInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.SignMessageSuccessResponse + +class ResponseSerializer : StdSerializer(Response::class.java) { + override fun serialize( + value: Response, + gen: JsonGenerator, + provider: SerializerProvider, + ) { + gen.writeStartObject() + if (value.resultType.isNotEmpty()) { + gen.writeStringField("result_type", value.resultType) + } + when (value) { + is NwcErrorResponse -> { + if (value.error != null) { + gen.writeObjectField("error", value.error) + } + } + + is PayInvoiceErrorResponse -> { + if (value.error != null) { + gen.writeObjectField("error", value.error) + } + } + + is PayInvoiceSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is PayKeysendSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is MakeInvoiceSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is LookupInvoiceSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is ListTransactionsSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is GetBalanceSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is GetInfoSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is MakeHoldInvoiceSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is CancelHoldInvoiceSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is SettleHoldInvoiceSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is GetBudgetSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is SignMessageSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + + is CreateConnectionSuccessResponse -> { + if (value.result != null) { + gen.writeObjectField("result", value.result) + } + } + } + gen.writeEndObject() + } +} From b96c1c2c3a90b5febf749db58b2775dcc6efc8c0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 16:38:31 -0400 Subject: [PATCH 147/172] Fixes iosGzip --- .../com/vitorpamplona/quartz/utils/GZip.ios.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt index 24676f8126..708f6f8153 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/GZip.ios.kt @@ -73,9 +73,14 @@ actual object GZip { val written = input.usePinned { pinIn -> output.usePinned { pinOut -> - stream.next_in = pinIn.addressOf(0).reinterpret() + if (input.isNotEmpty()) { + stream.next_in = pinIn.addressOf(0).reinterpret() + } stream.avail_in = input.size.toUInt() - stream.next_out = pinOut.addressOf(0).reinterpret() + + if (output.isNotEmpty()) { + stream.next_out = pinOut.addressOf(0).reinterpret() + } stream.avail_out = maxSize.toUInt() deflate(stream.ptr, Z_FINISH) @@ -97,6 +102,8 @@ actual object GZip { * Output is collected in fixed-size chunks to handle arbitrary output size. */ actual fun decompress(content: ByteArray): String { + if (content.isEmpty()) return "" + val chunks = ArrayList() val chunkSize = maxOf(content.size * 4, 4096) @@ -108,7 +115,9 @@ actual object GZip { .let { check(it == Z_OK) { "inflateInit2 failed: $it" } } content.usePinned { pinIn -> - stream.next_in = pinIn.addressOf(0).reinterpret() + if (content.isNotEmpty()) { + stream.next_in = pinIn.addressOf(0).reinterpret() + } stream.avail_in = content.size.toUInt() var status: Int = Z_OK From 95c7adc90b75907b481d0d6d8333150ef46a02bd Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 16:53:17 -0400 Subject: [PATCH 148/172] Less warnings --- .../amethyst/commons/search/QueryParserTest.kt | 2 +- .../com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt | 3 ++- .../vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt | 6 +++--- .../vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt | 4 ++-- .../vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt | 2 +- .../com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt | 2 +- .../amethyst/desktop/ui/auth/NewKeyWarningCard.kt | 2 +- .../amethyst/desktop/ui/search/SearchResultsList.kt | 3 ++- 8 files changed, 13 insertions(+), 11 deletions(-) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParserTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParserTest.kt index 4cae7f2c30..df5c3af77c 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParserTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/search/QueryParserTest.kt @@ -128,7 +128,7 @@ class QueryParserTest { fun untilDate() { val q = QueryParser.parse("until:2025-12-31") assertNull(q.since) - assertTrue(q.until != null && q.until!! > 0) + assertTrue(q.until != null && q.until > 0) } @Test diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt index daf10b553a..efc578373d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Add @@ -183,7 +184,7 @@ fun ChessScreen( Row(verticalAlignment = Alignment.CenterVertically) { if (selectedGameId != null) { IconButton(onClick = { viewModel.selectGame(null) }) { - Icon(Icons.Default.ArrowBack, "Back to list") + Icon(Icons.AutoMirrored.Default.ArrowBack, "Back to list") } Spacer(Modifier.width(8.dp)) } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt index e0f5086d1b..ae3725798d 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/model/DesktopIAccount.kt @@ -86,9 +86,9 @@ class DesktopIAccount( override val privateZapsDecryptionCache: IPrivateZapsDecryptionCache = object : IPrivateZapsDecryptionCache { - override fun cachedPrivateZap(zapRequest: LnZapRequestEvent): com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent? = null + override fun cachedPrivateZap(event: LnZapRequestEvent): com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent? = null - override suspend fun decryptPrivateZap(zapRequest: LnZapRequestEvent): com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent? = null + override suspend fun decryptPrivateZap(event: LnZapRequestEvent): com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent? = null } override fun userProfile(): User = localCache.getOrCreateUser(pubKey) @@ -108,7 +108,7 @@ class DesktopIAccount( override suspend fun sendNip04PrivateMessage(eventTemplate: EventTemplate) { if (!isWriteable()) return - val signedEvent = signer.sign(eventTemplate) + val signedEvent = signer.sign(eventTemplate) val recipient = signedEvent.verifiedRecipientPubKey() // Optimistic local add so the message appears immediately diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index eed9c760f3..75769e59fe 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -214,7 +214,7 @@ fun UserProfileScreen( latestMetadataEvent = event } } - } catch (e: Exception) { + } catch (_: Exception) { // Ignore parse errors } } @@ -332,7 +332,7 @@ fun UserProfileScreen( } // Edit button for own profile - if (isOwnProfile && account?.isReadOnly == false) { + if (isOwnProfile && account.isReadOnly == false) { OutlinedButton( onClick = { editingDisplayName = displayName ?: "" diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt index d8c7c16321..bae3732b17 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.desktop.ui.auth -import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -41,6 +40,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.login_hide_key diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt index 67d1bcdf76..ea6c001135 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.desktop.ui.auth -import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -54,6 +53,7 @@ import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.resources.Res diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt index f4a3c432c3..573d035861 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.desktop.ui.auth -import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -35,6 +34,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.resources.Res diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt index bb43400f96..e02889f840 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Article import androidx.compose.material.icons.filled.Article import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.ExpandMore @@ -185,7 +186,7 @@ fun SearchResultsList( SortableHeader( title = "Articles", count = articles.size, - icon = Icons.Default.Article, + icon = Icons.AutoMirrored.Default.Article, options = SearchSortOrder.EVENT_OPTIONS, selected = eventSortOrder, onSelect = { state.updateEventSortOrder(it) }, From 874d750bf03649a706ace8287730b507c5451c31 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 17:01:59 -0400 Subject: [PATCH 149/172] removing unused import --- .../amethyst/desktop/ui/search/SearchResultsList.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt index e02889f840..2a8a36085e 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/search/SearchResultsList.kt @@ -36,7 +36,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Article -import androidx.compose.material.icons.filled.Article import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Forum From 7e2912ffd8f995024c1f5b1126e7d3d0275c7c7b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 17:15:33 -0400 Subject: [PATCH 150/172] removes more warnings --- .../quartz/nip49PrivKeyEnc/NIP49Test.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt index c011e785ee..c0cee1c9d4 100644 --- a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip49PrivKeyEnc/NIP49Test.kt @@ -26,15 +26,15 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.fail -public class NIP49Test { +class NIP49Test { companion object { - val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p" + const val TEST_CASE = "ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p" - val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683" - val TEST_CASE_PASSWORD = "nostr" + const val TEST_CASE_EXPECTED = "3501454135014541350145413501453fefb02227e449e57cf4d3a3ce05378683" + const val TEST_CASE_PASSWORD = "nostr" val MAIN_TEST_CASES = - listOf( + listOf( Nip49TestCase(".ksjabdk.aselqwe", "14c226dbdd865d5e1645e72c7470fd0a17feb42cc87b750bab6538171b3a3f8a", 1, 0x00), Nip49TestCase("skjdaklrnçurbç l", "f7f2f77f98890885462764afb15b68eb5f69979c8046ecb08cad7c4ae6b221ab", 2, 0x01), Nip49TestCase("777z7z7z7z7z7z7z", "11b25a101667dd9208db93c0827c6bdad66729a5b521156a7e9d3b22b3ae8944", 3, 0x02), @@ -74,7 +74,7 @@ public class NIP49Test { @Test fun encryptDecryptTestCase() { val encrypted = nip49.encrypt(TEST_CASE_EXPECTED, TEST_CASE_PASSWORD, 16, 0) - val decrypted = nip49.decrypt(encrypted!!, TEST_CASE_PASSWORD) + val decrypted = nip49.decrypt(encrypted, TEST_CASE_PASSWORD) assertEquals(TEST_CASE_EXPECTED, decrypted) } @@ -86,7 +86,7 @@ public class NIP49Test { assertNotNull(encrypted) - val decrypted = nip49.decrypt(encrypted!!, it.password) + val decrypted = nip49.decrypt(encrypted, it.password) assertEquals(it.secretKey, decrypted) } @@ -105,7 +105,7 @@ public class NIP49Test { assertNotNull(encrypted) - val decrypted = nip49.decrypt(encrypted!!, samePassword2) + val decrypted = nip49.decrypt(encrypted, samePassword2) assertEquals(TEST_CASE_EXPECTED, decrypted) } From 12c27af8139ff147cf48c33f07275b090ed83430 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 17:16:05 -0400 Subject: [PATCH 151/172] cancel other running builds in github actions when a new one comes up --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9bb6fd2951..e5985a719d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,10 @@ on: push: branches: [main] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: lint: runs-on: ubuntu-latest From e7760dc7c728b83b226976697a9a595d2b9780ba Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 17:16:18 -0400 Subject: [PATCH 152/172] adds bundleId to the binary file --- quartz/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index b9ffc2add8..ade53340e1 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -122,12 +122,14 @@ kotlin { iosArm64 { binaries.framework { baseName = xcfName + binaryOption("bundleId", "com.vitorpamplona.quartz") } } iosSimulatorArm64 { binaries.framework { baseName = xcfName + binaryOption("bundleId", "com.vitorpamplona.quartz") } } From 7f61b4819b79e3fc84f4ee009c4bf132d83c22bf Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 17:45:05 -0400 Subject: [PATCH 153/172] new log fuctions mocked --- amethyst/src/test/java/android/util/Log.java | 4 ++++ quartz/src/commonTest/kotlin/android/util/Log.kt | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/amethyst/src/test/java/android/util/Log.java b/amethyst/src/test/java/android/util/Log.java index af85d0b47e..245a440a12 100644 --- a/amethyst/src/test/java/android/util/Log.java +++ b/amethyst/src/test/java/android/util/Log.java @@ -1,6 +1,10 @@ package android.util; public class Log { + public static Boolean isLoggable(String tag, Integer msg) { + return true; + } + public static int d(String tag, String msg) { System.out.println("DEBUG: " + tag + ": " + msg); return 0; diff --git a/quartz/src/commonTest/kotlin/android/util/Log.kt b/quartz/src/commonTest/kotlin/android/util/Log.kt index ba75a2cab1..eb9574b088 100644 --- a/quartz/src/commonTest/kotlin/android/util/Log.kt +++ b/quartz/src/commonTest/kotlin/android/util/Log.kt @@ -24,6 +24,12 @@ import kotlin.jvm.JvmStatic class Log { companion object { + @JvmStatic + fun isLoggable( + tag: String?, + msg: Int?, + ): Boolean = true + @JvmStatic fun d( tag: String?, From f708f6c2653e39827a11d2cb4a3d62a0c82d168b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 17:46:20 -0400 Subject: [PATCH 154/172] Specify which .def file is used between simulators and ios Devices --- quartz/build.gradle.kts | 43 +++++++++++++++---- .../src/nativeInterop/cinterop/Clibsodium.def | 3 -- 2 files changed, 34 insertions(+), 12 deletions(-) delete mode 100644 quartz/src/nativeInterop/cinterop/Clibsodium.def diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index ade53340e1..da783590ca 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -65,19 +65,36 @@ kotlin { val xcfName = "quartz-kmpKit" val libsodiumPath = project.file("src/nativeInterop/libsodium") val libsodiumHeaderFilesPath = project.file("$libsodiumPath/include/sodium") - val libsodiumDefFile = project.file("src/nativeInterop/cinterop/Clibsodium.def") + + // Generate target-specific Libsodium definition files for creating native bindings. + // Device (iosArm64) uses libsodium.a, simulator targets use libsodium-simulator.a. + val libsodiumDeviceDefFile = + project.layout.buildDirectory + .file("cinterop/Clibsodium-device.def") + .get() + .asFile + val libsodiumSimulatorDefFile = + project.layout.buildDirectory + .file("cinterop/Clibsodium-simulator.def") + .get() + .asFile // This generates the Libsodium definition file, necessary for creating native bindings(a Kotlin API) for libsodium(for iOS). val libsodiumDefFileGeneration = tasks.register("GenerateSodiumCinteropFile") { - outputs.file(libsodiumDefFile) + outputs.files(libsodiumDeviceDefFile, libsodiumSimulatorDefFile) doLast { - if (!libsodiumDefFile.exists()) { - libsodiumDefFile.parentFile.mkdirs() - libsodiumDefFile.writeText("package = Clibsodium\n") - libsodiumDefFile.appendText("staticLibraries = libsodium.a libsodium-simulator.a\n") - libsodiumDefFile.appendText("libraryPaths = ${libsodiumPath.absolutePath}/ios/lib ${libsodiumPath.absolutePath}/ios-simulators/lib\n") - } + libsodiumDeviceDefFile.parentFile.mkdirs() + libsodiumDeviceDefFile.writeText( + "package = Clibsodium\n" + + "staticLibraries = libsodium.a\n" + + "libraryPaths = ${libsodiumPath.absolutePath}/ios/lib\n", + ) + libsodiumSimulatorDefFile.writeText( + "package = Clibsodium\n" + + "staticLibraries = libsodium-simulator.a\n" + + "libraryPaths = ${libsodiumPath.absolutePath}/ios-simulators/lib\n", + ) } } @@ -85,10 +102,12 @@ kotlin { iosArm64(), iosSimulatorArm64(), ).forEach { target -> + val isSimulator = target.name != "iosArm64" + val defFile = if (isSimulator) libsodiumSimulatorDefFile else libsodiumDeviceDefFile target.compilations.getByName("main") { val clibsodium by cinterops.creating { - definitionFile = libsodiumDefFile + definitionFile = defFile packageName = "Clibsodium" headers( @@ -120,6 +139,9 @@ kotlin { } iosArm64 { + binaries.all { + linkerOpts("-L${libsodiumPath.absolutePath}/ios/lib", "-lsodium") + } binaries.framework { baseName = xcfName binaryOption("bundleId", "com.vitorpamplona.quartz") @@ -127,6 +149,9 @@ kotlin { } iosSimulatorArm64 { + binaries.all { + linkerOpts("-L${libsodiumPath.absolutePath}/ios-simulators/lib", "-lsodium-simulator") + } binaries.framework { baseName = xcfName binaryOption("bundleId", "com.vitorpamplona.quartz") diff --git a/quartz/src/nativeInterop/cinterop/Clibsodium.def b/quartz/src/nativeInterop/cinterop/Clibsodium.def deleted file mode 100644 index a60f3fda69..0000000000 --- a/quartz/src/nativeInterop/cinterop/Clibsodium.def +++ /dev/null @@ -1,3 +0,0 @@ -package = Clibsodium -staticLibraries = libsodium.a libsodium-simulator.a -libraryPaths = src/nativeInterop/libsodium/ios/lib src/nativeInterop/libsodium/ios-simulators/lib From 790fd45470bdb49958143ce266c058a0d07f0a5a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 18:10:55 -0400 Subject: [PATCH 155/172] Removes warnings --- .../vitorpamplona/quartz/utils/UriParser.ios.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.ios.kt index 05c06c5478..ba0890c38f 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.ios.kt @@ -26,32 +26,32 @@ import platform.Foundation.NSURLQueryItem actual class UriParser actual constructor( uri: String, ) { - private val nsUrlComponents: NSURLComponents? = NSURLComponents(string = uri) + private val nsUrlComponents: NSURLComponents = NSURLComponents(string = uri) - actual fun scheme(): String? = nsUrlComponents?.scheme + actual fun scheme(): String? = nsUrlComponents.scheme - actual fun host(): String? = nsUrlComponents?.host + actual fun host(): String? = nsUrlComponents.host actual fun port(): Int? { // The NSNumber?.intValue is a way to handle a nullable port and convert it // from a platform-specific number type to a Kotlin Int. - return nsUrlComponents?.port?.intValue + return nsUrlComponents.port?.intValue } - actual fun path(): String? = nsUrlComponents?.path + actual fun path(): String? = nsUrlComponents.path actual fun queryParameterNames(): Set { - val queryItems = nsUrlComponents?.queryItems ?: return emptySet() + val queryItems = nsUrlComponents.queryItems ?: return emptySet() return queryItems.mapNotNull { (it as? NSURLQueryItem)?.name }.toSet() } actual fun getQueryParameter(param: String): String? { - val queryItems = nsUrlComponents?.queryItems ?: return null + val queryItems = nsUrlComponents.queryItems ?: return null return (queryItems.firstOrNull { (it as? NSURLQueryItem)?.name == param } as? NSURLQueryItem)?.value } val fragments: Map by lazy { - nsUrlComponents?.fragment()?.ifBlank { null }?.let { keyValuePair -> + nsUrlComponents.fragment()?.ifBlank { null }?.let { keyValuePair -> keyValuePair.split('&').associate { paramValue -> val parts = paramValue.split("=", limit = 2) if (parts.size == 2) { From 6ffc902598f1fe3b2b794fdee68e00f10372796c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 18:15:43 -0400 Subject: [PATCH 156/172] Unifies UriParser --- .../quartz/utils/UriParser.android.kt | 61 ------------------- .../quartz/utils/UriParser.jvmAndroid.kt} | 16 ++--- 2 files changed, 8 insertions(+), 69 deletions(-) delete mode 100644 quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.android.kt rename quartz/src/{jvmMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvm.kt => jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvmAndroid.kt} (86%) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.android.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.android.kt deleted file mode 100644 index 7d157db80e..0000000000 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.android.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.quartz.utils - -import androidx.core.net.toUri -import java.net.URLDecoder - -actual class UriParser actual constructor( - uri: String, -) { - val myUri = uri.toUri() - - val fragments: Map by lazy { - myUri.fragment?.ifBlank { null }?.let { keyValuePair -> - keyValuePair.split('&').associate { paramValue -> - val parts = paramValue.split("=", limit = 2) - if (parts.size == 2) { - parts[0] to URLDecoder.decode(parts[1], "UTF-8") - } else { - parts[0] to "" // Handle parameters without a value, e.g., "param&other=value" - } - } - } ?: emptyMap() - } - - actual fun scheme(): String? = myUri.scheme - - actual fun host(): String? = myUri.host - - actual fun port(): Int? { - // android.net.Uri.getPort() returns -1 if the port is not set, so we handle that case. - val port = myUri.port - return if (port == -1) null else port - } - - actual fun path(): String? = myUri.path - - actual fun queryParameterNames() = myUri.queryParameterNames - - actual fun getQueryParameter(param: String) = myUri.getQueryParameter(param) - - actual fun fragments(): Map = fragments -} diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvm.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvmAndroid.kt similarity index 86% rename from quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvm.kt rename to quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvmAndroid.kt index ebdf489700..3e75d0dc87 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvm.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/utils/UriParser.jvmAndroid.kt @@ -26,28 +26,29 @@ import java.net.URLDecoder actual class UriParser actual constructor( uri: String, ) { - val myUri = URI.create(uri) - val queryParameters: Map by lazy { + private val myUri = URI.create(uri) + + private val queryParameters: Map by lazy { myUri.query?.ifBlank { null }?.let { query -> query.split('&').associate { paramValue -> val parts = paramValue.split("=", limit = 2) if (parts.size == 2) { - parts[0] to parts[1] + parts[0] to URLDecoder.decode(parts[1], "UTF-8") } else { - parts[0] to "" // Handle parameters without a value, e.g., "param&other=value" + parts[0] to "" // Handle parameters without a value } } } ?: emptyMap() } - val fragments: Map by lazy { + private val fragments: Map by lazy { myUri.rawFragment?.ifBlank { null }?.let { keyValuePair -> keyValuePair.split('&').associate { paramValue -> val parts = paramValue.split("=", limit = 2) if (parts.size == 2) { parts[0] to URLDecoder.decode(parts[1], "UTF-8") } else { - parts[0] to "" // Handle parameters without a value, e.g., "param&other=value" + parts[0] to "" // Handle parameters without a value } } } ?: emptyMap() @@ -58,7 +59,6 @@ actual class UriParser actual constructor( actual fun host(): String? = myUri.host actual fun port(): Int? { - // java.net.URI.getPort() returns -1 if the port is not set, so we handle that case. val port = myUri.port return if (port == -1) null else port } @@ -69,5 +69,5 @@ actual class UriParser actual constructor( actual fun getQueryParameter(param: String): String? = queryParameters[param] - actual fun fragments() = fragments + actual fun fragments(): Map = fragments } From b0c714176a9327ee31010522380145b9d4a965a7 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 18:46:39 -0400 Subject: [PATCH 157/172] Updates versions for github actions --- .github/workflows/build.yml | 38 ++++++++++++++-------------- .github/workflows/create-release.yml | 12 ++++----- .github/workflows/crowdin.yml | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5985a719d..0377b37359 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,16 +16,16 @@ jobs: timeout-minutes: 15 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - name: Cache gradle - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -50,16 +50,16 @@ jobs: shell: bash steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - name: Cache gradle - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -76,7 +76,7 @@ jobs: if: ${{ always() && matrix.os == 'ubuntu-latest' }} - name: Upload Test Results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: ${{ always() && matrix.os == 'ubuntu-latest' }} with: name: Test Reports @@ -88,16 +88,16 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - name: Cache gradle - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -110,13 +110,13 @@ jobs: run: ./gradlew assembleDebug - name: Upload Play APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: Play Debug APK path: amethyst/build/outputs/apk/play/debug/amethyst-play-universal-debug.apk - name: Upload FDroid APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: FDroid Debug APK path: amethyst/build/outputs/apk/fdroid/debug/amethyst-fdroid-universal-debug.apk @@ -125,19 +125,19 @@ jobs: run: ./gradlew assembleBenchmark - name: Upload Play APK Benchmark - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: Play Benchmark APK path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk - name: Upload FDroid APK Benchmark - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: FDroid Benchmark APK path: amethyst/build/outputs/apk/fdroid/benchmark/amethyst-fdroid-universal-benchmark.apk - name: Upload Compose Reports - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: Compose Reports path: amethyst/build/compose_compiler @@ -167,16 +167,16 @@ jobs: shell: bash steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - name: Cache gradle - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -189,7 +189,7 @@ jobs: run: ./gradlew :desktopApp:${{ matrix.task }} - name: Upload Desktop Distribution - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: ${{ matrix.artifact-name }} path: ${{ matrix.artifact-path }} diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 70f337ecdf..beb08874b5 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -27,16 +27,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - name: Cache gradle - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches @@ -263,16 +263,16 @@ jobs: shell: bash steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 - name: Cache gradle - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.gradle/caches diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index d2707654d0..923e7fa492 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: crowdin action uses: crowdin/github-action@v2 From 962da3280bd421630188a07d198bb01b957a2446 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 18:57:00 -0400 Subject: [PATCH 158/172] Moves the N44 test to Jvmtest --- .../com/vitorpamplona/quartz/nip44Encryption/Nip44v2JvmTest.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename quartz/src/{commonTest/kotlin => jvmTest/java}/com/vitorpamplona/quartz/nip44Encryption/Nip44v2JvmTest.kt (100%) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2JvmTest.kt b/quartz/src/jvmTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2JvmTest.kt similarity index 100% rename from quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip44Encryption/Nip44v2JvmTest.kt rename to quartz/src/jvmTest/java/com/vitorpamplona/quartz/nip44Encryption/Nip44v2JvmTest.kt From 56cce46858f1e16a158a9e47325e49f7d39d86b0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 19:22:28 -0400 Subject: [PATCH 159/172] Solving all problems with Quartz tests --- quartz/build.gradle.kts | 13 ++- .../LnZapPaymentRequestNip44EventTest.kt | 84 +++++++++++++++++++ .../LnZapPaymentRequestEventTest.kt | 49 ----------- .../LnZapPaymentRequestNip44EventTest.kt | 81 ++++++++++++++++++ .../LnZapPaymentRequestNip44EventTest.kt | 81 ++++++++++++++++++ 5 files changed, 258 insertions(+), 50 deletions(-) create mode 100644 quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt create mode 100644 quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt create mode 100644 quartz/src/jvmTest/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt diff --git a/quartz/build.gradle.kts b/quartz/build.gradle.kts index da783590ca..f0d4f68153 100644 --- a/quartz/build.gradle.kts +++ b/quartz/build.gradle.kts @@ -274,8 +274,10 @@ kotlin { } getByName("androidHostTest") { - dependsOn(jvmAndroidTest) dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + // Bitcoin secp256k1 bindings implementation(libs.secp256k1.kmp.jni.jvm) @@ -291,7 +293,16 @@ kotlin { implementation(libs.androidx.core) implementation(libs.androidx.junit) implementation(libs.androidx.espresso.core) + + implementation(libs.kotlin.test) implementation(libs.kotlinx.coroutines.test) + + // Bitcoin secp256k1 bindings to Android + api(libs.secp256k1.kmp.jni.android) + + // LibSodium for ChaCha encryption (NIP-44) + implementation("com.goterl:lazysodium-android:5.2.0@aar") + implementation("net.java.dev.jna:jna:5.18.1@aar") } } diff --git a/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt new file mode 100644 index 0000000000..9175d08180 --- /dev/null +++ b/quartz/src/androidDeviceTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +@RunWith(AndroidJUnit4::class) +class LnZapPaymentRequestNip44EventTest { + @Test + fun testCreateRequestWithNip44() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.toHexKey() + + val request = GetBalanceMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + createdAt = 1000L, + useNip44 = true, + ) + + assertEquals(23194, event.kind) + assertEquals("nip44_v2", event.encryptionScheme()) + } + + @Test + fun testDecryptNip44Request() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletSigner = NostrSignerInternal(walletKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.toHexKey() + + val request = GetInfoMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + useNip44 = true, + ) + + assertEquals("nip44_v2", event.encryptionScheme()) + + // Wallet service should be able to decrypt NIP-44 encrypted request + val decrypted = event.decryptRequest(walletSigner) + assertIs(decrypted) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt index 271c2783b2..18a1f577ce 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestEventTest.kt @@ -86,29 +86,6 @@ class LnZapPaymentRequestEventTest { assertNull(event.encryptionScheme()) } - @Test - fun testCreateRequestWithNip44() = - runTest { - val clientKeyPair = KeyPair() - val walletKeyPair = KeyPair() - val clientSigner = NostrSignerInternal(clientKeyPair) - val walletServicePubkey: HexKey = - walletKeyPair.pubKey.toHexKey() - - val request = GetBalanceMethod.create() - val event = - LnZapPaymentRequestEvent.createRequest( - request = request, - walletServicePubkey = walletServicePubkey, - signer = clientSigner, - createdAt = 1000L, - useNip44 = true, - ) - - assertEquals(23194, event.kind) - assertEquals("nip44_v2", event.encryptionScheme()) - } - @Test fun testDecryptPayInvoiceRequest() = runTest { @@ -156,32 +133,6 @@ class LnZapPaymentRequestEventTest { assertEquals("test payment", decrypted.params?.description) } - @Test - fun testDecryptNip44Request() = - runTest { - val clientKeyPair = KeyPair() - val walletKeyPair = KeyPair() - val clientSigner = NostrSignerInternal(clientKeyPair) - val walletSigner = NostrSignerInternal(walletKeyPair) - val walletServicePubkey: HexKey = - walletKeyPair.pubKey.toHexKey() - - val request = GetInfoMethod.create() - val event = - LnZapPaymentRequestEvent.createRequest( - request = request, - walletServicePubkey = walletServicePubkey, - signer = clientSigner, - useNip44 = true, - ) - - assertEquals("nip44_v2", event.encryptionScheme()) - - // Wallet service should be able to decrypt NIP-44 encrypted request - val decrypted = event.decryptRequest(walletSigner) - assertIs(decrypted) - } - @Test fun testCanDecrypt() = runTest { diff --git a/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt new file mode 100644 index 0000000000..fa07bc58e9 --- /dev/null +++ b/quartz/src/iosTest/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class LnZapPaymentRequestNip44EventTest { + @Test + fun testCreateRequestWithNip44() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.toHexKey() + + val request = GetBalanceMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + createdAt = 1000L, + useNip44 = true, + ) + + assertEquals(23194, event.kind) + assertEquals("nip44_v2", event.encryptionScheme()) + } + + @Test + fun testDecryptNip44Request() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletSigner = NostrSignerInternal(walletKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.toHexKey() + + val request = GetInfoMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + useNip44 = true, + ) + + assertEquals("nip44_v2", event.encryptionScheme()) + + // Wallet service should be able to decrypt NIP-44 encrypted request + val decrypted = event.decryptRequest(walletSigner) + assertIs(decrypted) + } +} diff --git a/quartz/src/jvmTest/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt b/quartz/src/jvmTest/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt new file mode 100644 index 0000000000..fa07bc58e9 --- /dev/null +++ b/quartz/src/jvmTest/java/com/vitorpamplona/quartz/nip47WalletConnect/LnZapPaymentRequestNip44EventTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip47WalletConnect + +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class LnZapPaymentRequestNip44EventTest { + @Test + fun testCreateRequestWithNip44() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.toHexKey() + + val request = GetBalanceMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + createdAt = 1000L, + useNip44 = true, + ) + + assertEquals(23194, event.kind) + assertEquals("nip44_v2", event.encryptionScheme()) + } + + @Test + fun testDecryptNip44Request() = + runTest { + val clientKeyPair = KeyPair() + val walletKeyPair = KeyPair() + val clientSigner = NostrSignerInternal(clientKeyPair) + val walletSigner = NostrSignerInternal(walletKeyPair) + val walletServicePubkey: HexKey = + walletKeyPair.pubKey.toHexKey() + + val request = GetInfoMethod.create() + val event = + LnZapPaymentRequestEvent.createRequest( + request = request, + walletServicePubkey = walletServicePubkey, + signer = clientSigner, + useNip44 = true, + ) + + assertEquals("nip44_v2", event.encryptionScheme()) + + // Wallet service should be able to decrypt NIP-44 encrypted request + val decrypted = event.decryptRequest(walletSigner) + assertIs(decrypted) + } +} From d0297b7e343c1ee46e0d3dc8081c410f954d4ea1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 16 Mar 2026 19:44:51 -0400 Subject: [PATCH 160/172] Fixes stability plugin for AGP9 --- amethyst/build.gradle | 11 +++++++++++ gradle/libs.versions.toml | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index b6b3c6f2aa..70b0379087 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -36,6 +36,17 @@ def generateVersionName(String baseVersion) { } } +// Workaround: stability.analyzer plugin doesn't declare task dependencies properly for Gradle 9.x +afterEvaluate { + def stabilityNames = tasks.names.findAll { it.contains("StabilityCheck") } + def compileNames = tasks.names.findAll { it.matches("compile.*UnitTestKotlin") } + stabilityNames.each { scName -> + compileNames.each { ctName -> + tasks.named(scName).configure { mustRunAfter(tasks.named(ctName)) } + } + } +} + android { namespace = 'com.vitorpamplona.amethyst' compileSdk = libs.versions.android.compileSdk.get().toInteger() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9fda673e2e..d6a7174668 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,6 +69,7 @@ kotlinTest = "2.3.0" core = "1.7.0" mavenPublish = "0.36.0" spmForKmpVersion = "1.4.10" +stabilityAnalyser = "0.7.0" [libraries] abedElazizShe-video-compressor-fork = { group = "com.github.davotoula", name = "LightCompressor-enhanced", version.ref = "lightcompressor-enhanced" } @@ -187,6 +188,6 @@ serialization = { id = 'org.jetbrains.kotlin.plugin.serialization', version.ref kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } -stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version = "0.7.0" } +stability-analyzer = { id = "com.github.skydoves.compose.stability.analyzer", version.ref = "stabilityAnalyser" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } frankois944-spmForKmp = { id = "io.github.frankois944.spmForKmp", version.ref = "spmForKmpVersion" } From 146f3f91b51336add8f583a22569a7341fc011c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 02:20:47 +0000 Subject: [PATCH 161/172] feat: add GitHub Action to build benchmark APK on claude branches Adds a lightweight workflow that triggers on pushes to claude/* branches, building only the Play Benchmark APK for quick testing after Claude completes a task. https://claude.ai/code/session_013hcKzBgJgfsrArSgTTGNEk --- .github/workflows/build-benchmark-apk.yml | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/build-benchmark-apk.yml diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml new file mode 100644 index 0000000000..cb1d8dd212 --- /dev/null +++ b/.github/workflows/build-benchmark-apk.yml @@ -0,0 +1,43 @@ +name: Build Benchmark APK + +on: + push: + branches: + - 'claude/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-benchmark: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: 'zulu' + java-version: 21 + + - name: Cache gradle + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build Benchmark APK + run: ./gradlew assemblePlayBenchmark + + - name: Upload Play Benchmark APK + uses: actions/upload-artifact@v6 + with: + name: Play Benchmark APK + path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk From 61e85df8dbb07b60ee44dca0963fa34191dc1458 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 02:27:53 +0000 Subject: [PATCH 162/172] feat: post commit comment when benchmark APK is ready Adds a GitHub Script step that comments on the commit with a direct link to the workflow artifacts, making it visible in PRs and branch pages. https://claude.ai/code/session_013hcKzBgJgfsrArSgTTGNEk --- .github/workflows/build-benchmark-apk.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index cb1d8dd212..a9e338b314 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -37,7 +37,20 @@ jobs: run: ./gradlew assemblePlayBenchmark - name: Upload Play Benchmark APK + id: upload uses: actions/upload-artifact@v6 with: name: Play Benchmark APK path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk + + - name: Comment on commit with APK link + uses: actions/github-script@v7 + with: + script: | + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + await github.rest.repos.createCommitComment({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: context.sha, + body: `📦 **Benchmark APK ready!**\n\nDownload it from the [workflow run artifacts](${runUrl}#artifacts).` + }); From 39e15a89295ab2bf642181488c1f9868d0ede1b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:23:00 +0000 Subject: [PATCH 163/172] feat: comment benchmark APK link on open PRs Add PR commenting to the build-benchmark-apk workflow. When a build completes, it now finds any open PR for the branch and posts a comment with a direct download link to the benchmark APK artifact. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index a9e338b314..e3efc36746 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -48,9 +48,30 @@ jobs: with: script: | const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const artifactUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${{ steps.upload.outputs.artifact-id }}`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${artifactUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; + await github.rest.repos.createCommitComment({ owner: context.repo.owner, repo: context.repo.repo, commit_sha: context.sha, - body: `📦 **Benchmark APK ready!**\n\nDownload it from the [workflow run artifacts](${runUrl}#artifacts).` + body }); + + // Find open PRs for this branch and comment on them + const branch = context.ref.replace('refs/heads/', ''); + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:${branch}`, + state: 'open' + }); + + for (const pr of prs) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); + } From af12d13d67229650bcd97825b10f9225dbaa5201 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:34:53 +0000 Subject: [PATCH 164/172] fix: use artifact-url output for direct APK download link The manually constructed URL was invalid. Use the artifact-url output from upload-artifact which provides the correct download URL. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index e3efc36746..54db24f5ec 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -48,7 +48,7 @@ jobs: with: script: | const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const artifactUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${{ steps.upload.outputs.artifact-id }}`; + const artifactUrl = `${{ steps.upload.outputs.artifact-url }}`; const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${artifactUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; await github.rest.repos.createCommitComment({ From 1c4f9769b12c2e494ff8e5ef5c5ad633c4accb0c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:43:10 +0000 Subject: [PATCH 165/172] fix: session-start hook fails silently when dl.google.com is blocked The Claude Code web egress proxy does not include dl.google.com in its allowlist, causing the session-start hook to abort entirely (due to set -euo pipefail) when Android SDK downloads fail. This cascading failure also prevented local.properties creation and ANDROID_HOME export. Additionally, JAVA_TOOL_OPTIONS contains nonProxyHosts entries for *.google.com and *.googleapis.com, which causes the JVM to bypass the proxy for Google Maven, resulting in connection failures. Changes: - Remove set -euo pipefail; use per-section error handling instead - Fix JAVA_TOOL_OPTIONS: strip *.google.com from nonProxyHosts - Fix no_proxy: remove *.google.com and *.googleapis.com entries - Move local.properties and env exports before SDK download section - Make SDK downloads non-fatal with clear warning messages - Install standalone ktlint as fallback for spotlessApply - Create spotless-apply wrapper script for proxy-blocked environments - Update Stop hook to fall back to spotless-apply when Gradle fails https://claude.ai/code/session_01HeWiRdKDTnMGfEBUegk3d1 --- .claude/hooks/session-start.sh | 232 ++++++++++++++++++++++++--------- .claude/settings.json | 2 +- 2 files changed, 170 insertions(+), 64 deletions(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 35cddfb3b4..b17f6e97ba 100644 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,12 +1,59 @@ #!/bin/bash # Session start hook: Configure proxy auth, SSL trust, and Android SDK for Claude Code on the web -set -euo pipefail # Only run in remote (web) environments if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi +# --- Fix no_proxy: remove *.google.com and *.googleapis.com so traffic routes through proxy --- +# The default no_proxy in Claude Code web includes *.google.com and *.googleapis.com, +# which causes JVM/curl to bypass the proxy for Google Maven (dl.google.com). +# These domains MUST go through the proxy for authenticated egress to work. +fix_no_proxy() { + local var="$1" + local val="${!var:-}" + if [ -n "$val" ]; then + # Remove *.google.com, *.googleapis.com entries from the comma-separated list + local fixed + fixed=$(echo "$val" | tr ',' '\n' | grep -v -E '^\*\.google\.com$|^\*\.googleapis\.com$' | tr '\n' ',' | sed 's/,$//') + export "$var=$fixed" + fi +} + +fix_no_proxy no_proxy +fix_no_proxy NO_PROXY + +# --- Fix JAVA_TOOL_OPTIONS: remove nonProxyHosts that bypass Google domains --- +# JAVA_TOOL_OPTIONS may contain -Dhttp.nonProxyHosts=...|*.google.com|*.googleapis.com +# which prevents the JVM from routing Google Maven traffic through the proxy. +if [ -n "${JAVA_TOOL_OPTIONS:-}" ]; then + # Remove the nonProxyHosts entries for google.com and googleapis.com from the JVM options + # Strategy: rewrite the -Dhttp.nonProxyHosts value to exclude google patterns + JAVA_TOOL_OPTIONS=$(echo "$JAVA_TOOL_OPTIONS" | sed -E ' + s/-Dhttp\.nonProxyHosts=[^ ]*/\n&\n/g + ' | while IFS= read -r line; do + if [[ "$line" == -Dhttp.nonProxyHosts=* ]]; then + # Extract the value, remove google patterns, reconstruct + val="${line#-Dhttp.nonProxyHosts=}" + fixed=$(echo "$val" | tr '|' '\n' | grep -v -E '^\*\.google\.com$|^\*\.googleapis\.com$' | tr '\n' '|' | sed 's/|$//') + echo "-Dhttp.nonProxyHosts=$fixed" + else + echo "$line" + fi + done | tr '\n' ' ' | sed 's/ */ /g; s/^ //; s/ $//') + export JAVA_TOOL_OPTIONS +fi + +# Export fixed env vars for the session +if [ -n "${CLAUDE_ENV_FILE:-}" ]; then + echo "export no_proxy='${no_proxy:-}'" >> "$CLAUDE_ENV_FILE" + echo "export NO_PROXY='${NO_PROXY:-}'" >> "$CLAUDE_ENV_FILE" + echo "export JAVA_TOOL_OPTIONS='$JAVA_TOOL_OPTIONS'" >> "$CLAUDE_ENV_FILE" +fi + +echo "Fixed proxy bypass settings (no_proxy, JAVA_TOOL_OPTIONS)" >&2 + # --- Proxy credentials: configure Maven/Gradle if authenticated proxy is set --- proxy="${https_proxy:-${HTTPS_PROXY:-}}" if [ -n "$proxy" ] && echo "$proxy" | grep -q '@'; then @@ -99,68 +146,11 @@ if [ -n "$ANTHROPIC_CA_PEM" ]; then rm -f "$TMPCA" fi +# --- Project setup: local.properties and env vars (before SDK download so these always run) --- ANDROID_SDK_DIR="/root/android-sdk" -SDK_REPO_BASE="https://dl.google.com/android/repository" - -# Install Android SDK packages by downloading directly with curl -# (sdkmanager cannot reach the SDK repository through the proxy) -install_sdk_package() { - local zip_url="$1" - local dest_dir="$2" - local inner_dir="$3" # top-level dir inside the zip - - if [ -d "$dest_dir" ]; then - return 0 - fi - - echo "Downloading $zip_url..." - local TMP_ZIP - TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) - curl -fsSL "$zip_url" -o "$TMP_ZIP" - - local TMP_DIR - TMP_DIR=$(mktemp -d) - unzip -q "$TMP_ZIP" -d "$TMP_DIR" - rm -f "$TMP_ZIP" - - mkdir -p "$(dirname "$dest_dir")" - mv "$TMP_DIR/$inner_dir" "$dest_dir" - rm -rf "$TMP_DIR" - echo "Installed to $dest_dir" -} - -# Install Android platform 36 -install_sdk_package \ - "$SDK_REPO_BASE/platform-36_r02.zip" \ - "$ANDROID_SDK_DIR/platforms/android-36" \ - "android-36" - -# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) -install_sdk_package \ - "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ - "$ANDROID_SDK_DIR/build-tools/36.0.0" \ - "android-16" - -# Install platform-tools -install_sdk_package \ - "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ - "$ANDROID_SDK_DIR/platform-tools" \ - "platform-tools" - -# Accept SDK licenses (create license files manually) -echo "Writing SDK license files..." -mkdir -p "$ANDROID_SDK_DIR/licenses" -# android-sdk-license -echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" -echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" -# android-sdk-preview-license -echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" -echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" -# intel-android-extra-license -echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" +REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/amethyst}")" # Create local.properties if missing -REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/Amber}")" LOCAL_PROPS="$REPO_ROOT/local.properties" if [ ! -f "$LOCAL_PROPS" ]; then echo "sdk.dir=$ANDROID_SDK_DIR" > "$LOCAL_PROPS" @@ -174,7 +164,123 @@ if [ -n "${CLAUDE_ENV_FILE:-}" ]; then echo "export PATH=\$PATH:$ANDROID_SDK_DIR/platform-tools" >> "$CLAUDE_ENV_FILE" fi -cd "$CLAUDE_PROJECT_DIR" -./gradlew --version > /dev/null 2>&1 +# --- Install standalone ktlint (fallback for spotlessApply when Gradle can't resolve AGP) --- +# The egress proxy may block dl.google.com (Google Maven), preventing Gradle from resolving +# the Android Gradle Plugin. Since spotlessApply depends on Gradle's plugin resolution, +# we install ktlint standalone as a reliable alternative. +KTLINT_VERSION="1.5.0" +KTLINT_BIN="/usr/local/bin/ktlint" +if [ ! -x "$KTLINT_BIN" ]; then + echo "Installing ktlint $KTLINT_VERSION..." >&2 + if curl -fsSL "https://github.com/pinterest/ktlint/releases/download/${KTLINT_VERSION}/ktlint" -o "$KTLINT_BIN" 2>/dev/null; then + chmod +x "$KTLINT_BIN" + echo "Installed ktlint $KTLINT_VERSION to $KTLINT_BIN" >&2 + else + echo "WARNING: Failed to install ktlint (GitHub may be unreachable)" >&2 + fi +fi -echo "Android SDK setup complete." \ No newline at end of file +# Create a spotless-apply wrapper that mimics the Gradle spotless configuration +cat > /usr/local/bin/spotless-apply << 'SPOTLESS_SCRIPT' +#!/bin/bash +# Standalone spotless-apply: runs ktlint with the same settings as the Gradle spotless plugin. +# Use this when ./gradlew spotlessApply fails due to proxy/network issues. +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +cd "$REPO_ROOT" + +if ! command -v ktlint &>/dev/null; then + echo "ERROR: ktlint not found. Install it first." >&2 + exit 1 +fi + +echo "Running ktlint format on Kotlin sources..." +# Find all Kotlin source files matching the spotless target pattern (src/**/*.kt) +# Exclude build directories +ktlint --format "**/*.kt" 2>&1 | grep -v "^$" || true + +echo "Done. Files formatted with ktlint." +SPOTLESS_SCRIPT +chmod +x /usr/local/bin/spotless-apply + +# --- Android SDK download (non-fatal: warns if dl.google.com is blocked) --- +SDK_REPO_BASE="https://dl.google.com/android/repository" + +install_sdk_package() { + local zip_url="$1" + local dest_dir="$2" + local inner_dir="$3" # top-level dir inside the zip + + if [ -d "$dest_dir" ]; then + return 0 + fi + + echo "Downloading $zip_url..." >&2 + local TMP_ZIP + TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) + if ! curl -fsSL "$zip_url" -o "$TMP_ZIP" 2>/dev/null; then + rm -f "$TMP_ZIP" + echo "WARNING: Failed to download $zip_url (dl.google.com may be blocked by proxy)" >&2 + echo " -> Add dl.google.com to the egress proxy allowlist to fix this" >&2 + return 1 + fi + + local TMP_DIR + TMP_DIR=$(mktemp -d) + unzip -q "$TMP_ZIP" -d "$TMP_DIR" + rm -f "$TMP_ZIP" + + mkdir -p "$(dirname "$dest_dir")" + mv "$TMP_DIR/$inner_dir" "$dest_dir" + rm -rf "$TMP_DIR" + echo "Installed to $dest_dir" >&2 +} + +sdk_ok=true + +# Install Android platform 36 +if ! install_sdk_package \ + "$SDK_REPO_BASE/platform-36_r02.zip" \ + "$ANDROID_SDK_DIR/platforms/android-36" \ + "android-36"; then + sdk_ok=false +fi + +# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) +if ! install_sdk_package \ + "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ + "$ANDROID_SDK_DIR/build-tools/36.0.0" \ + "android-16"; then + sdk_ok=false +fi + +# Install platform-tools +if ! install_sdk_package \ + "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ + "$ANDROID_SDK_DIR/platform-tools" \ + "platform-tools"; then + sdk_ok=false +fi + +if [ "$sdk_ok" = true ]; then + # Accept SDK licenses (create license files manually) + echo "Writing SDK license files..." >&2 + mkdir -p "$ANDROID_SDK_DIR/licenses" + echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" + echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" + echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" + echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" + echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" + echo "Android SDK setup complete." >&2 +else + echo "WARNING: Android SDK download failed. dl.google.com is likely blocked by the egress proxy." >&2 + echo " -> Gradle builds requiring AGP will fail until dl.google.com is added to the proxy allowlist." >&2 + echo " -> Use 'spotless-apply' (standalone ktlint) instead of './gradlew spotlessApply'." >&2 +fi + +# Warm up Gradle wrapper (non-fatal) +cd "$REPO_ROOT" +./gradlew --version > /dev/null 2>&1 || true + +echo "Session start hook complete." >&2 diff --git a/.claude/settings.json b/.claude/settings.json index 80496cbbab..4ab4d17262 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -16,7 +16,7 @@ "hooks": [ { "type": "command", - "command": "./gradlew spotlessApply", + "command": "./gradlew spotlessApply 2>/dev/null || spotless-apply", "timeout": 120 } ] From 883a2f995e22dc35cb848c390065be1cf09cf2a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:43:19 +0000 Subject: [PATCH 166/172] style: apply ktlint formatting fixes https://claude.ai/code/session_01HeWiRdKDTnMGfEBUegk3d1 --- .../amethyst/ui/components/ShareHelper.kt | 11 ++++++----- .../ui/screen/loggedIn/chess/ChessLobbyScreen.kt | 6 ++++-- .../loggedIn/hashtag/dal/HashtagFeedFilter.kt | 3 ++- .../screen/loggedIn/home/ShortNotePostViewModel.kt | 3 ++- .../gallery/dal/UserProfileGalleryFeedFilter.kt | 13 +++++++------ .../amethyst/desktop/chess/ChessScreen.kt | 7 +++++-- .../quartz/nip01Core/store/sqlite/QueryBuilder.kt | 3 ++- .../namecoin/NamecoinNameResolver.kt | 3 ++- .../quartz/nip64Chess/ChessStateReconstructor.kt | 9 ++++++--- .../quartz/nip64Chess/ChessEngine.jvmAndroid.kt | 3 ++- .../nip05/namecoin/NamecoinNameResolverTest.kt | 3 ++- 11 files changed, 40 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ShareHelper.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ShareHelper.kt index 32aeaa6792..90bad3c7f0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ShareHelper.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ShareHelper.kt @@ -119,11 +119,12 @@ object ShareHelper { bytesRead >= 12 && matchesMagicNumbers(header, 4, MOV_FTYP) -> detectMp4OrMov(header) // MP4/MOV alternative: moov, mdat, or free at offset 4 - bytesRead >= 8 && ( - matchesMagicNumbers(header, 4, MOV_MOOV) || - matchesMagicNumbers(header, 4, MOV_MDAT) || - matchesMagicNumbers(header, 4, MOV_FREE) - ) -> "mp4" + bytesRead >= 8 && + ( + matchesMagicNumbers(header, 4, MOV_MOOV) || + matchesMagicNumbers(header, 4, MOV_MDAT) || + matchesMagicNumbers(header, 4, MOV_FREE) + ) -> "mp4" else -> defaultExtension } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt index ad9ad0e22e..96a2502ebf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chess/ChessLobbyScreen.kt @@ -297,8 +297,10 @@ fun ChessLobbyContent( val userPubkey = accountViewModel.account.userProfile().pubkeyHex val hasContent = - activeGames.isNotEmpty() || spectatingGames.isNotEmpty() || - publicGames.isNotEmpty() || challenges.isNotEmpty() + activeGames.isNotEmpty() || + spectatingGames.isNotEmpty() || + publicGames.isNotEmpty() || + challenges.isNotEmpty() if (!hasContent) { // Empty state - use LazyColumn so pull-to-refresh works diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt index 9a95a0e46b..e9c468e941 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/dal/HashtagFeedFilter.kt @@ -83,7 +83,8 @@ class HashtagFeedFilter( event is PrivateDmEvent || event is PollNoteEvent || event is AudioHeaderEvent - ) && event.isTaggedHash(hashTag) + ) && + event.isTaggedHash(hashTag) fun acceptableViaScope( event: Event?, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 4b63c9df40..87fd5939ac 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -1047,7 +1047,8 @@ open class ShortNotePostViewModel : ( !wantsPoll || ( - pollOptions.isNotEmpty() && pollOptions.all { it.value.label.isNotEmpty() } && + pollOptions.isNotEmpty() && + pollOptions.all { it.value.label.isNotEmpty() } && closedAt > TimeUtils.oneMinuteFromNow() ) ) && diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt index 0a8f6f31d4..0e943641de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/dal/UserProfileGalleryFeedFilter.kt @@ -77,12 +77,13 @@ class UserProfileGalleryFeedFilter( val noteEvent = it.event return ( ( - it.event?.pubKey == user.pubkeyHex && ( - noteEvent is PictureEvent || - noteEvent is RegularVideoEvent || - (noteEvent is ReplaceableVideoEvent && it is AddressableNote) || - (noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent()) - ) + it.event?.pubKey == user.pubkeyHex && + ( + noteEvent is PictureEvent || + noteEvent is RegularVideoEvent || + (noteEvent is ReplaceableVideoEvent && it is AddressableNote) || + (noteEvent is ProfileGalleryEntryEvent && noteEvent.hasUrl() && noteEvent.hasFromEvent()) + ) ) // && noteEvent.isOneOf(SUPPORTED_VIDEO_FEED_MIME_TYPES_SET)) ) && params.match(noteEvent, it.relays) && diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt index efc578373d..2668756b86 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/chess/ChessScreen.kt @@ -345,8 +345,11 @@ private fun ChessLobby( listState: LazyListState = rememberLazyListState(), ) { val hasContent = - activeGames.isNotEmpty() || spectatingGames.isNotEmpty() || - publicGames.isNotEmpty() || challenges.isNotEmpty() || completedGames.isNotEmpty() + activeGames.isNotEmpty() || + spectatingGames.isNotEmpty() || + publicGames.isNotEmpty() || + challenges.isNotEmpty() || + completedGames.isNotEmpty() if (!hasContent) { // Empty state diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt index c876c7b962..8ab91cfaeb 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/sqlite/QueryBuilder.kt @@ -678,7 +678,8 @@ class QueryBuilder( val search: String? = null, ) { fun isSimpleSearch() = - search != null && search.isNotEmpty() && + search != null && + search.isNotEmpty() && (nonDTagsIn == null || nonDTagsIn.isEmpty()) && (nonDTagsAll == null || nonDTagsAll.isEmpty()) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/NamecoinNameResolver.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/NamecoinNameResolver.kt index 72abf7546a..4d4a44aa82 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/NamecoinNameResolver.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip05DnsIdentifiers/namecoin/NamecoinNameResolver.kt @@ -301,7 +301,8 @@ class NamecoinNameResolver( pubkey = rootMatch.content } - firstEntry != null && firstEntry.value is JsonPrimitive && + firstEntry != null && + firstEntry.value is JsonPrimitive && isValidPubkey((firstEntry.value as JsonPrimitive).content) -> { resolvedLocalPart = firstEntry.key pubkey = (firstEntry.value as JsonPrimitive).content diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessStateReconstructor.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessStateReconstructor.kt index 997e9921c7..4dd69fbd45 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessStateReconstructor.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessStateReconstructor.kt @@ -217,9 +217,12 @@ object ChessStateReconstructor { return fen1 == fen2 // Fallback to exact match } - return parts1[0] == parts2[0] && // Board position - parts1[1] == parts2[1] && // Active color - parts1[2] == parts2[2] && // Castling rights + return parts1[0] == parts2[0] && + // Board position + parts1[1] == parts2[1] && + // Active color + parts1[2] == parts2[2] && + // Castling rights parts1[3] == parts2[3] // En passant } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt index d9b192d9db..dbd0bbb271 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip64Chess/ChessEngine.jvmAndroid.kt @@ -212,7 +212,8 @@ actual class ChessEngine { board.getPiece(toSquare) != Piece.NONE || ( pt == com.github.bhlangonijr.chesslib.PieceType.PAWN && - epTarget != Square.NONE && toSquare == epTarget + epTarget != Square.NONE && + toSquare == epTarget ) if (pt != com.github.bhlangonijr.chesslib.PieceType.PAWN) { diff --git a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip05/namecoin/NamecoinNameResolverTest.kt b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip05/namecoin/NamecoinNameResolverTest.kt index 52822c49cb..0c975e7c33 100644 --- a/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip05/namecoin/NamecoinNameResolverTest.kt +++ b/quartz/src/jvmTest/kotlin/com/vitorpamplona/quartz/nip05/namecoin/NamecoinNameResolverTest.kt @@ -261,7 +261,8 @@ class NamecoinNameResolverTest { pubkey = rootMatch.content } - firstEntry != null && firstEntry.value is kotlinx.serialization.json.JsonPrimitive && + firstEntry != null && + firstEntry.value is kotlinx.serialization.json.JsonPrimitive && (firstEntry.value as kotlinx.serialization.json.JsonPrimitive) .content .matches(Regex("^[0-9a-fA-F]{64}$")) -> { From 6bc927147c5e592e7140e77bd127647341f477e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 03:47:47 +0000 Subject: [PATCH 167/172] fix: use nightly.link for direct APK download without auth GitHub artifact URLs require authentication and don't provide direct file downloads. nightly.link proxies artifact downloads, giving a direct download link that works for anyone without GitHub login. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index 54db24f5ec..beaddc41bb 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -48,8 +48,9 @@ jobs: with: script: | const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const artifactUrl = `${{ steps.upload.outputs.artifact-url }}`; - const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${artifactUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; + const artifactName = encodeURIComponent('Play Benchmark APK'); + const nightlyUrl = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/${artifactName}.zip`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${nightlyUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; await github.rest.repos.createCommitComment({ owner: context.repo.owner, From 61dc903e9a5cc21bb011dc573108301d47cea47a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 04:03:18 +0000 Subject: [PATCH 168/172] fix: use artifact ID for direct download URL instead of nightly.link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use the artifact-id output from upload-artifact to construct a direct GitHub artifact download URL. No third-party service needed — the URL points to the artifact on GitHub which triggers a download when clicked. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index beaddc41bb..1f19c0e678 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -47,10 +47,10 @@ jobs: uses: actions/github-script@v7 with: script: | + const artifactId = `${{ steps.upload.outputs.artifact-id }}`; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const artifactName = encodeURIComponent('Play Benchmark APK'); - const nightlyUrl = `https://nightly.link/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/${artifactName}.zip`; - const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${nightlyUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; + const downloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${artifactId}`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${downloadUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; await github.rest.repos.createCommitComment({ owner: context.repo.owner, From 706d282681198321ed817e117b53dcaab7dd54aa Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 04:05:33 +0000 Subject: [PATCH 169/172] fix: direct APK download via prerelease asset, PR comment only - Upload APK as a GitHub prerelease asset for direct .apk download (no zip wrapping like artifacts) - Remove commit comment, only comment on open PRs - Clean up existing release with same tag before creating https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 69 ++++++++++++++++++----- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index 1f19c0e678..ba9c7ee3c4 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -36,30 +36,69 @@ jobs: - name: Build Benchmark APK run: ./gradlew assemblePlayBenchmark - - name: Upload Play Benchmark APK + - name: Upload APK to prerelease id: upload - uses: actions/upload-artifact@v6 - with: - name: Play Benchmark APK - path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk - - - name: Comment on commit with APK link uses: actions/github-script@v7 with: script: | - const artifactId = `${{ steps.upload.outputs.artifact-id }}`; - const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - const downloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${artifactId}`; - const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${downloadUrl})\n\nOr view all artifacts in the [workflow run](${runUrl}#artifacts).`; + const fs = require('fs'); + const apkPath = 'amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk'; + const sha = context.sha.substring(0, 7); + const tag = `benchmark-${sha}`; - await github.rest.repos.createCommitComment({ + // Delete existing release with this tag if it exists + try { + const { data: existing } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag + }); + await github.rest.repos.deleteRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: existing.id + }); + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${tag}` + }); + } catch (e) { + // Release doesn't exist yet, that's fine + } + + const { data: release } = await github.rest.repos.createRelease({ owner: context.repo.owner, repo: context.repo.repo, - commit_sha: context.sha, - body + tag_name: tag, + target_commitish: context.sha, + name: `Benchmark APK (${sha})`, + body: `Auto-generated benchmark APK for commit ${context.sha}`, + prerelease: true }); - // Find open PRs for this branch and comment on them + const apkData = fs.readFileSync(apkPath); + const { data: asset } = await github.rest.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + name: 'amethyst-play-universal-benchmark.apk', + data: apkData, + headers: { + 'content-type': 'application/vnd.android.package-archive', + 'content-length': apkData.length + } + }); + + core.setOutput('apk-url', asset.browser_download_url); + + - name: Comment on PR with APK link + uses: actions/github-script@v7 + with: + script: | + const apkUrl = `${{ steps.upload.outputs.apk-url }}`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${apkUrl})`; + const branch = context.ref.replace('refs/heads/', ''); const { data: prs } = await github.rest.pulls.list({ owner: context.repo.owner, From dbf5d6954c73eafb61f847fe70a15a4c6ea41e58 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 04:08:30 +0000 Subject: [PATCH 170/172] fix: revert to artifact zip download, PR comment only Use upload-artifact with artifact-id for direct zip download URL. Remove prerelease approach and commit comments. https://claude.ai/code/session_0138kUcZaPQoQcAc7nA1KDbf --- .github/workflows/build-benchmark-apk.yml | 62 +++-------------------- 1 file changed, 7 insertions(+), 55 deletions(-) diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index ba9c7ee3c4..1326569d94 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -36,68 +36,20 @@ jobs: - name: Build Benchmark APK run: ./gradlew assemblePlayBenchmark - - name: Upload APK to prerelease + - name: Upload Play Benchmark APK id: upload - uses: actions/github-script@v7 + uses: actions/upload-artifact@v6 with: - script: | - const fs = require('fs'); - const apkPath = 'amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk'; - const sha = context.sha.substring(0, 7); - const tag = `benchmark-${sha}`; - - // Delete existing release with this tag if it exists - try { - const { data: existing } = await github.rest.repos.getReleaseByTag({ - owner: context.repo.owner, - repo: context.repo.repo, - tag - }); - await github.rest.repos.deleteRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: existing.id - }); - await github.rest.git.deleteRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: `tags/${tag}` - }); - } catch (e) { - // Release doesn't exist yet, that's fine - } - - const { data: release } = await github.rest.repos.createRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - tag_name: tag, - target_commitish: context.sha, - name: `Benchmark APK (${sha})`, - body: `Auto-generated benchmark APK for commit ${context.sha}`, - prerelease: true - }); - - const apkData = fs.readFileSync(apkPath); - const { data: asset } = await github.rest.repos.uploadReleaseAsset({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: release.id, - name: 'amethyst-play-universal-benchmark.apk', - data: apkData, - headers: { - 'content-type': 'application/vnd.android.package-archive', - 'content-length': apkData.length - } - }); - - core.setOutput('apk-url', asset.browser_download_url); + name: Play Benchmark APK + path: amethyst/build/outputs/apk/play/benchmark/amethyst-play-universal-benchmark.apk - name: Comment on PR with APK link uses: actions/github-script@v7 with: script: | - const apkUrl = `${{ steps.upload.outputs.apk-url }}`; - const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${apkUrl})`; + const artifactId = `${{ steps.upload.outputs.artifact-id }}`; + const downloadUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts/${artifactId}`; + const body = `📦 **Benchmark APK ready!**\n\nDownload: [Play Benchmark APK](${downloadUrl})`; const branch = context.ref.replace('refs/heads/', ''); const { data: prs } = await github.rest.pulls.list({ From 9bf3086230c25f5f5dc19a52e0311e849e31b3f6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 17 Mar 2026 09:25:33 -0400 Subject: [PATCH 171/172] Make the session-start executable by default --- .claude/hooks/session-start.sh | 0 .github/workflows/build-benchmark-apk.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 .claude/hooks/session-start.sh diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh old mode 100644 new mode 100755 diff --git a/.github/workflows/build-benchmark-apk.yml b/.github/workflows/build-benchmark-apk.yml index 1326569d94..0a65ca6c1d 100644 --- a/.github/workflows/build-benchmark-apk.yml +++ b/.github/workflows/build-benchmark-apk.yml @@ -1,4 +1,4 @@ -name: Build Benchmark APK +name: Build APK For Claude on: push: From baa55ab7f9c271dead14c88597f13be37bdd64a2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 17 Mar 2026 09:55:56 -0400 Subject: [PATCH 172/172] reverts to amber's session start --- .claude/hooks/session-start.sh | 232 +++++++++------------------------ 1 file changed, 63 insertions(+), 169 deletions(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index b17f6e97ba..35cddfb3b4 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -1,59 +1,12 @@ #!/bin/bash # Session start hook: Configure proxy auth, SSL trust, and Android SDK for Claude Code on the web +set -euo pipefail # Only run in remote (web) environments if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then exit 0 fi -# --- Fix no_proxy: remove *.google.com and *.googleapis.com so traffic routes through proxy --- -# The default no_proxy in Claude Code web includes *.google.com and *.googleapis.com, -# which causes JVM/curl to bypass the proxy for Google Maven (dl.google.com). -# These domains MUST go through the proxy for authenticated egress to work. -fix_no_proxy() { - local var="$1" - local val="${!var:-}" - if [ -n "$val" ]; then - # Remove *.google.com, *.googleapis.com entries from the comma-separated list - local fixed - fixed=$(echo "$val" | tr ',' '\n' | grep -v -E '^\*\.google\.com$|^\*\.googleapis\.com$' | tr '\n' ',' | sed 's/,$//') - export "$var=$fixed" - fi -} - -fix_no_proxy no_proxy -fix_no_proxy NO_PROXY - -# --- Fix JAVA_TOOL_OPTIONS: remove nonProxyHosts that bypass Google domains --- -# JAVA_TOOL_OPTIONS may contain -Dhttp.nonProxyHosts=...|*.google.com|*.googleapis.com -# which prevents the JVM from routing Google Maven traffic through the proxy. -if [ -n "${JAVA_TOOL_OPTIONS:-}" ]; then - # Remove the nonProxyHosts entries for google.com and googleapis.com from the JVM options - # Strategy: rewrite the -Dhttp.nonProxyHosts value to exclude google patterns - JAVA_TOOL_OPTIONS=$(echo "$JAVA_TOOL_OPTIONS" | sed -E ' - s/-Dhttp\.nonProxyHosts=[^ ]*/\n&\n/g - ' | while IFS= read -r line; do - if [[ "$line" == -Dhttp.nonProxyHosts=* ]]; then - # Extract the value, remove google patterns, reconstruct - val="${line#-Dhttp.nonProxyHosts=}" - fixed=$(echo "$val" | tr '|' '\n' | grep -v -E '^\*\.google\.com$|^\*\.googleapis\.com$' | tr '\n' '|' | sed 's/|$//') - echo "-Dhttp.nonProxyHosts=$fixed" - else - echo "$line" - fi - done | tr '\n' ' ' | sed 's/ */ /g; s/^ //; s/ $//') - export JAVA_TOOL_OPTIONS -fi - -# Export fixed env vars for the session -if [ -n "${CLAUDE_ENV_FILE:-}" ]; then - echo "export no_proxy='${no_proxy:-}'" >> "$CLAUDE_ENV_FILE" - echo "export NO_PROXY='${NO_PROXY:-}'" >> "$CLAUDE_ENV_FILE" - echo "export JAVA_TOOL_OPTIONS='$JAVA_TOOL_OPTIONS'" >> "$CLAUDE_ENV_FILE" -fi - -echo "Fixed proxy bypass settings (no_proxy, JAVA_TOOL_OPTIONS)" >&2 - # --- Proxy credentials: configure Maven/Gradle if authenticated proxy is set --- proxy="${https_proxy:-${HTTPS_PROXY:-}}" if [ -n "$proxy" ] && echo "$proxy" | grep -q '@'; then @@ -146,11 +99,68 @@ if [ -n "$ANTHROPIC_CA_PEM" ]; then rm -f "$TMPCA" fi -# --- Project setup: local.properties and env vars (before SDK download so these always run) --- ANDROID_SDK_DIR="/root/android-sdk" -REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/amethyst}")" +SDK_REPO_BASE="https://dl.google.com/android/repository" + +# Install Android SDK packages by downloading directly with curl +# (sdkmanager cannot reach the SDK repository through the proxy) +install_sdk_package() { + local zip_url="$1" + local dest_dir="$2" + local inner_dir="$3" # top-level dir inside the zip + + if [ -d "$dest_dir" ]; then + return 0 + fi + + echo "Downloading $zip_url..." + local TMP_ZIP + TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) + curl -fsSL "$zip_url" -o "$TMP_ZIP" + + local TMP_DIR + TMP_DIR=$(mktemp -d) + unzip -q "$TMP_ZIP" -d "$TMP_DIR" + rm -f "$TMP_ZIP" + + mkdir -p "$(dirname "$dest_dir")" + mv "$TMP_DIR/$inner_dir" "$dest_dir" + rm -rf "$TMP_DIR" + echo "Installed to $dest_dir" +} + +# Install Android platform 36 +install_sdk_package \ + "$SDK_REPO_BASE/platform-36_r02.zip" \ + "$ANDROID_SDK_DIR/platforms/android-36" \ + "android-36" + +# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) +install_sdk_package \ + "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ + "$ANDROID_SDK_DIR/build-tools/36.0.0" \ + "android-16" + +# Install platform-tools +install_sdk_package \ + "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ + "$ANDROID_SDK_DIR/platform-tools" \ + "platform-tools" + +# Accept SDK licenses (create license files manually) +echo "Writing SDK license files..." +mkdir -p "$ANDROID_SDK_DIR/licenses" +# android-sdk-license +echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" +echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" +# android-sdk-preview-license +echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" +echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" +# intel-android-extra-license +echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" # Create local.properties if missing +REPO_ROOT="$(git -C "$(dirname "$0")" rev-parse --show-toplevel 2>/dev/null || echo "${CLAUDE_PROJECT_DIR:-/home/user/Amber}")" LOCAL_PROPS="$REPO_ROOT/local.properties" if [ ! -f "$LOCAL_PROPS" ]; then echo "sdk.dir=$ANDROID_SDK_DIR" > "$LOCAL_PROPS" @@ -164,123 +174,7 @@ if [ -n "${CLAUDE_ENV_FILE:-}" ]; then echo "export PATH=\$PATH:$ANDROID_SDK_DIR/platform-tools" >> "$CLAUDE_ENV_FILE" fi -# --- Install standalone ktlint (fallback for spotlessApply when Gradle can't resolve AGP) --- -# The egress proxy may block dl.google.com (Google Maven), preventing Gradle from resolving -# the Android Gradle Plugin. Since spotlessApply depends on Gradle's plugin resolution, -# we install ktlint standalone as a reliable alternative. -KTLINT_VERSION="1.5.0" -KTLINT_BIN="/usr/local/bin/ktlint" -if [ ! -x "$KTLINT_BIN" ]; then - echo "Installing ktlint $KTLINT_VERSION..." >&2 - if curl -fsSL "https://github.com/pinterest/ktlint/releases/download/${KTLINT_VERSION}/ktlint" -o "$KTLINT_BIN" 2>/dev/null; then - chmod +x "$KTLINT_BIN" - echo "Installed ktlint $KTLINT_VERSION to $KTLINT_BIN" >&2 - else - echo "WARNING: Failed to install ktlint (GitHub may be unreachable)" >&2 - fi -fi +cd "$CLAUDE_PROJECT_DIR" +./gradlew --version > /dev/null 2>&1 -# Create a spotless-apply wrapper that mimics the Gradle spotless configuration -cat > /usr/local/bin/spotless-apply << 'SPOTLESS_SCRIPT' -#!/bin/bash -# Standalone spotless-apply: runs ktlint with the same settings as the Gradle spotless plugin. -# Use this when ./gradlew spotlessApply fails due to proxy/network issues. -set -euo pipefail - -REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" -cd "$REPO_ROOT" - -if ! command -v ktlint &>/dev/null; then - echo "ERROR: ktlint not found. Install it first." >&2 - exit 1 -fi - -echo "Running ktlint format on Kotlin sources..." -# Find all Kotlin source files matching the spotless target pattern (src/**/*.kt) -# Exclude build directories -ktlint --format "**/*.kt" 2>&1 | grep -v "^$" || true - -echo "Done. Files formatted with ktlint." -SPOTLESS_SCRIPT -chmod +x /usr/local/bin/spotless-apply - -# --- Android SDK download (non-fatal: warns if dl.google.com is blocked) --- -SDK_REPO_BASE="https://dl.google.com/android/repository" - -install_sdk_package() { - local zip_url="$1" - local dest_dir="$2" - local inner_dir="$3" # top-level dir inside the zip - - if [ -d "$dest_dir" ]; then - return 0 - fi - - echo "Downloading $zip_url..." >&2 - local TMP_ZIP - TMP_ZIP=$(mktemp /tmp/sdk-pkg.XXXXXX.zip) - if ! curl -fsSL "$zip_url" -o "$TMP_ZIP" 2>/dev/null; then - rm -f "$TMP_ZIP" - echo "WARNING: Failed to download $zip_url (dl.google.com may be blocked by proxy)" >&2 - echo " -> Add dl.google.com to the egress proxy allowlist to fix this" >&2 - return 1 - fi - - local TMP_DIR - TMP_DIR=$(mktemp -d) - unzip -q "$TMP_ZIP" -d "$TMP_DIR" - rm -f "$TMP_ZIP" - - mkdir -p "$(dirname "$dest_dir")" - mv "$TMP_DIR/$inner_dir" "$dest_dir" - rm -rf "$TMP_DIR" - echo "Installed to $dest_dir" >&2 -} - -sdk_ok=true - -# Install Android platform 36 -if ! install_sdk_package \ - "$SDK_REPO_BASE/platform-36_r02.zip" \ - "$ANDROID_SDK_DIR/platforms/android-36" \ - "android-36"; then - sdk_ok=false -fi - -# Install build-tools 36.0.0 (zip uses "android-16" as inner dir name) -if ! install_sdk_package \ - "$SDK_REPO_BASE/build-tools_r36_linux.zip" \ - "$ANDROID_SDK_DIR/build-tools/36.0.0" \ - "android-16"; then - sdk_ok=false -fi - -# Install platform-tools -if ! install_sdk_package \ - "$SDK_REPO_BASE/platform-tools_r37.0.0-linux.zip" \ - "$ANDROID_SDK_DIR/platform-tools" \ - "platform-tools"; then - sdk_ok=false -fi - -if [ "$sdk_ok" = true ]; then - # Accept SDK licenses (create license files manually) - echo "Writing SDK license files..." >&2 - mkdir -p "$ANDROID_SDK_DIR/licenses" - echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_SDK_DIR/licenses/android-sdk-license" - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" >> "$ANDROID_SDK_DIR/licenses/android-sdk-license" - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" - echo -e "\n504667f4c0de7af1a06de9f4b1727b84351f2910" >> "$ANDROID_SDK_DIR/licenses/android-sdk-preview-license" - echo -e "\nd975f751698a77b662f1254ddbeed3901e976f5a" > "$ANDROID_SDK_DIR/licenses/intel-android-extra-license" - echo "Android SDK setup complete." >&2 -else - echo "WARNING: Android SDK download failed. dl.google.com is likely blocked by the egress proxy." >&2 - echo " -> Gradle builds requiring AGP will fail until dl.google.com is added to the proxy allowlist." >&2 - echo " -> Use 'spotless-apply' (standalone ktlint) instead of './gradlew spotlessApply'." >&2 -fi - -# Warm up Gradle wrapper (non-fatal) -cd "$REPO_ROOT" -./gradlew --version > /dev/null 2>&1 || true - -echo "Session start hook complete." >&2 +echo "Android SDK setup complete." \ No newline at end of file