From f110436853094324f62481e4e4272c1426aeb3e7 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 31 Mar 2026 19:47:41 +0000 Subject: [PATCH 1/2] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 18 +++++++++ .../src/main/res/values-ru-rRU/strings.xml | 37 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 0ef2ee2d2..50bdddb4e 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -377,8 +377,14 @@ Zakładki Domyślne zakładki Twoje domyślne zakładki obsługiwane przez większość klientów + Stare zakładki + Twoje stare zakładki + Przenieś wszystko do nowych zakładek + Zakładki pomyślnie przeniesione Projekty Ankiety + Zdjęcia + Filmiki Prywatne Zakładki Publiczne zakładki Dodaj do prywatnych zakładek @@ -652,6 +658,16 @@ Przeczytane Nowe wiadomości Nowe zapy + Reakcje + Powiadamia Cię, gdy ktoś zareaguje na twój post + %1$s zareagował na Twój post + dla %1$s + Nowe reakcje + Pojedynek szachowy + Powiadamia Cię o wydarzeniach w szachach + %1$s zaakceptował Twoją ofertę + %1$s wykonał ruch — teraz Twoja kolej + Aktualizacje szachów Powiadom: Dołącz do dyskusji ID Użytkownika lub Grupy @@ -1092,6 +1108,7 @@ Powiadomienia Wszystkie Filmiki + Zdjęcia Szachy Portfel Saldo @@ -1478,6 +1495,7 @@ Autentykacja Blossom Transmitery Nadawcze Lista zakładek + Stara lista zakładek Data spotkania Kalendarz Spotkanie diff --git a/amethyst/src/main/res/values-ru-rRU/strings.xml b/amethyst/src/main/res/values-ru-rRU/strings.xml index d06dfe9c8..30bc52c74 100644 --- a/amethyst/src/main/res/values-ru-rRU/strings.xml +++ b/amethyst/src/main/res/values-ru-rRU/strings.xml @@ -86,6 +86,7 @@ Название канала Моя новая группа URL фотографии + Url (необязательно) Описание Описание не найдено "О нас.. " @@ -102,6 +103,8 @@ Байт Ошибка Ошибки + Процент успешных соединений к трансляции + Количество ошибок соединения в этой сессии Домашняя лента Лента личных сообщений Лента чатов @@ -131,9 +134,17 @@ Скачивание видео было запущено… Загрузка вложений было запущено… Не удалось сохранить фото + Видео сохранено в галерею Не удалось сохранить видео Загрузить фото + Загрузить файл + Сделать фото + Снять видео Загрузка… + Глубокий + Высокий + Нейтральный + Обрабатывает ваш голос. Внимание: основные изменения тона потенциально могут быть отменены определенными слушателями. Пользователь не установил Lightning адрес для получения чаевых "ответить.. " Копирует ID записи для отправки @@ -176,6 +187,11 @@ Нужно принять условия использования Нужно ввести ключ Войти + Создать аккаунт + Как нам вас называть? + Нет учетной записи Nostr? + Уже есть учетная запись Nostr? + Создать новый аккаунт Сгенерировать ключ Загрузка ленты Загрузка аккаунта @@ -216,8 +232,13 @@ Отметить все новые прочитанными Отметить всё прочитанным Резервное копирование + Для дополнительной безопасности вы можете зашифровать ваш ключ с помощью пароля. Этот ключ начинается с **ncryptsec1** и не может быть использован без пароля. + \n\nЕсли вы потеряете свой пароль, вы не сможете восстановить ваш ключ. + + Не удалось зашифровать ваш приватный ключ Приватный ключ (nsec) скопирован Скопировать мой приватный ключ + Зашифровать и копировать мой секретный ключ Ошибка входа Биометрия не смогла подтвердить личность владельца этого телефона Биометрия не смогла подтвердить личность владельца этого телефона. Ошибка: %1$s @@ -261,6 +282,7 @@ @string/impersonation @string/nudity @string/illegal_behavior + Вредоносное ПО Блокировать и скрыть пользователя для вас. Ваши записи останутся видны тем, кого вы заблокировали. Блокированные пользователи перечислены на экране «Фильтры безопасности». Пожаловаться @@ -273,6 +295,11 @@ Заблокировать и пожаловаться Блокировать Закладки + Закладки + Ваши закладки + Старые закладки + Опросы + Изображения Личные закладки Публичные закладки Добавить в Личные закладки @@ -286,17 +313,24 @@ Секрет Wallet Connect Показать секрет nsec / приватный ключ в hex + Определяет, как отображается ваша личность, когда вы отправляете зап. + Подключить кошелек Сумма взноса в sat Отправить опрос Обязательные поля: Получатели запов Описание опроса + Вариант ответа Вариант %s Описание варианта Опциональные поля: Минимальный зап Максимальный зап Консенсус + (0–100)% + Один вариант ответа + Несколько вариантов ответа + Дата закрытия опроса & Время Закрытие после дней Невозможно проголосовать @@ -331,6 +365,8 @@ Без следа в Nostr, обычный Lightning платеж Сервер для загрузки LnAddress или @User + Список Amethyst. Вы можете добавить его индивидуально или добавить список. + Загрузка Ваши релеи (NIP-95) Файл хранится на ваших релеях. Новый NIP: может не работать в других клиентах Настройка Tor/Orbot @@ -350,6 +386,7 @@ \n6. Нажмите кнопку активации, чтобы использовать Orbot как прокси-сервер Порт Orbot Socks + Денежные операции Неверный порт Исп. Orbot Отключить Tor/Orbot From a4e3625f2689ad6bb45d915d73528508cc06177f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 31 Mar 2026 16:52:24 -0400 Subject: [PATCH 2/2] Migrates to the new 10008 profile badges kind --- .../amethyst/model/LocalCache.kt | 13 ++- .../metadata/FilterFollowsAndMutesFromKey.kt | 6 +- .../datasource/FilterUserProfileMetadata.kt | 6 +- .../profile/header/badges/DisplayBadges.kt | 55 +++++++++--- .../loggedIn/relays/RelayInformationScreen.kt | 6 +- amethyst/src/main/res/values/strings.xml | 1 + .../AcceptedBadgeSetEvent.kt} | 8 +- .../accepted/TagArrayBuilderExt.kt | 26 ++++++ .../{profiles => accepted}/TagArrayExt.kt | 4 +- .../tags/AcceptedBadge.kt | 2 +- .../nip58Badges/profile/ProfileBadgesEvent.kt | 89 +++++++++++++++++++ .../TagArrayBuilderExt.kt | 6 +- .../quartz/nip58Badges/profile/TagArrayExt.kt | 32 +++++++ .../nip58Badges/profile/tags/AcceptedBadge.kt | 71 +++++++++++++++ .../quartz/utils/EventFactory.kt | 6 +- 15 files changed, 296 insertions(+), 35 deletions(-) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/{profiles/BadgeProfilesEvent.kt => accepted/AcceptedBadgeSetEvent.kt} (94%) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayBuilderExt.kt rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/{profiles => accepted}/TagArrayExt.kt (92%) rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/{profiles => accepted}/tags/AcceptedBadge.kt (97%) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/ProfileBadgesEvent.kt rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/{profiles => profile}/TagArrayBuilderExt.kt (87%) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/tags/AcceptedBadge.kt 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 b948f67cb..31fd76865 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -168,9 +168,10 @@ import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent -import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent +import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent @@ -843,7 +844,12 @@ object LocalCache : ILocalCache, ICacheProvider { event.taggedAddresses().map { getOrCreateAddressableNote(it) } } - is BadgeProfilesEvent -> { + is AcceptedBadgeSetEvent -> { + event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } + + event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) } + } + + is ProfileBadgesEvent -> { event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } + event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) } } @@ -2539,6 +2545,7 @@ object LocalCache : ILocalCache, ICacheProvider { ): Boolean = try { when (event) { + is AcceptedBadgeSetEvent -> consumeBaseReplaceable(event, relay, wasVerified) is AdvertisedRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is AppDefinitionEvent -> consumeBaseReplaceable(event, relay, wasVerified) is AppRecommendationEvent -> consumeBaseReplaceable(event, relay, wasVerified) @@ -2551,7 +2558,6 @@ object LocalCache : ILocalCache, ICacheProvider { is AudioTrackEvent -> consumeBaseReplaceable(event, relay, wasVerified) is BadgeAwardEvent -> consumeRegularEvent(event, relay, wasVerified) is BadgeDefinitionEvent -> consumeBaseReplaceable(event, relay, wasVerified) - is BadgeProfilesEvent -> consumeBaseReplaceable(event, relay, wasVerified) is BlockedRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is BlossomServersEvent -> consumeBaseReplaceable(event, relay, wasVerified) is BroadcastRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) @@ -2635,6 +2641,7 @@ object LocalCache : ILocalCache, ICacheProvider { is PictureEvent -> consumeRegularEvent(event, relay, wasVerified) is PrivateDmEvent -> consumeRegularEvent(event, relay, wasVerified) is PrivateOutboxRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) + is ProfileBadgesEvent -> consumeBaseReplaceable(event, relay, wasVerified) is ProxyRelayListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is PinListEvent -> consumeBaseReplaceable(event, relay, wasVerified) is PublicMessageEvent -> consumeRegularEvent(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt index 51c7061ef..43051262b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/account/metadata/FilterFollowsAndMutesFromKey.kt @@ -30,14 +30,16 @@ import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEve import com.vitorpamplona.quartz.nip51Lists.followList.FollowListEvent import com.vitorpamplona.quartz.nip51Lists.muteList.MuteListEvent import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent -import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent +import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent +import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent val FollowAndMutesFromKeyKinds = listOf( PeopleListEvent.KIND, FollowListEvent.KIND, MuteListEvent.KIND, - BadgeProfilesEvent.KIND, + AcceptedBadgeSetEvent.KIND, + ProfileBadgesEvent.KIND, EmojiPackSelectionEvent.KIND, EphemeralChatListEvent.KIND, ChannelListEvent.KIND, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt index d6655b4c8..2115dc570 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileMetadata.kt @@ -26,7 +26,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent -import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent +import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent +import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent val UserProfileMetadataKinds = @@ -34,7 +35,8 @@ val UserProfileMetadataKinds = MetadataEvent.KIND, AdvertisedRelayListEvent.KIND, ContactListEvent.KIND, - BadgeProfilesEvent.KIND, + AcceptedBadgeSetEvent.KIND, + ProfileBadgesEvent.KIND, ) fun filterUserProfileMetadata( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt index cfcdc773a..9992202f9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -32,13 +32,14 @@ import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.EventFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEvent -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEventAndMapNotNull import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav @@ -49,11 +50,18 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.BadgePictureModifier import com.vitorpamplona.amethyst.ui.theme.Size35Modifier import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent -import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent +import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn @Composable fun DisplayBadges( @@ -61,25 +69,44 @@ fun DisplayBadges( accountViewModel: AccountViewModel, nav: INav, ) { - LoadAddressableNote( - BadgeProfilesEvent.createAddress(baseUser.pubkeyHex), - accountViewModel, - ) { note -> - if (note != null) { - WatchAndRenderBadgeList(note, accountViewModel, nav) - } - } + val oldDesign = AcceptedBadgeSetEvent.createAddress(baseUser.pubkeyHex) + val newDesign = ProfileBadgesEvent.createAddress(baseUser.pubkeyHex) + + val oldNote = accountViewModel.getOrCreateAddressableNote(oldDesign) + val newNote = accountViewModel.getOrCreateAddressableNote(newDesign) + + WatchAndRenderBadgeList(oldNote, newNote, accountViewModel, nav) } @Composable private fun WatchAndRenderBadgeList( - note: AddressableNote, + oldNote: AddressableNote, + newNote: AddressableNote, accountViewModel: AccountViewModel, nav: INav, ) { - val badgeList by observeNoteEventAndMapNotNull(note, accountViewModel) { event: BadgeProfilesEvent -> - event.badgeAwardEvents().toImmutableList() - } + // Subscribe in the relay for changes in this note. + EventFinderFilterAssemblerSubscription(oldNote, accountViewModel) + EventFinderFilterAssemblerSubscription(newNote, accountViewModel) + + // Subscribe in the LocalCache for changes that arrive in the device + val flow = + remember(oldNote, newNote) { + combine( + oldNote.flow().metadata.stateFlow, + newNote.flow().metadata.stateFlow, + ) { oldNote, newNote -> + val oldProfileBadgeEvent = oldNote.note.event as? AcceptedBadgeSetEvent + val newProfileBadgeEvent = newNote.note.event as? ProfileBadgesEvent + + newProfileBadgeEvent?.badgeAwardEvents()?.toImmutableList() + ?: oldProfileBadgeEvent?.badgeAwardEvents()?.toImmutableList() + }.distinctUntilChanged() + .flowOn(Dispatchers.IO) + } + + // Subscribe in the LocalCache for changes that arrive in the device + val badgeList by flow.collectAsStateWithLifecycle(persistentListOf()) badgeList?.let { list -> RenderBadgeList(list, accountViewModel, nav) } } 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 f55fae3d9..bb6b5e0f0 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 @@ -233,9 +233,10 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent -import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent +import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip62RequestToVanish.RequestToVanishEvent @@ -552,6 +553,7 @@ fun RelayInformationBody( @Suppress("DEPRECATION") fun kindDisplayName(kind: Int): Int = when (kind) { + AcceptedBadgeSetEvent.KIND -> R.string.kind_accepted_badge_set AdvertisedRelayListEvent.KIND -> R.string.kind_outbox_relays AppDefinitionEvent.KIND -> R.string.kind_apps AppRecommendationEvent.KIND -> R.string.kind_app_recommendations @@ -564,7 +566,6 @@ fun kindDisplayName(kind: Int): Int = AttestorProficiencyEvent.KIND -> R.string.attestor_proficiency BadgeAwardEvent.KIND -> R.string.kind_badge_awards BadgeDefinitionEvent.KIND -> R.string.kind_badge_definitions - BadgeProfilesEvent.KIND -> R.string.kind_profile_badges BlockedRelayListEvent.KIND -> R.string.kind_blocked_relays BlossomServersEvent.KIND -> R.string.kind_blossom_servers BlossomAuthorizationEvent.KIND -> R.string.kind_blossom_auth @@ -651,6 +652,7 @@ fun kindDisplayName(kind: Int): Int = OtsEvent.KIND -> R.string.kind_ots PaymentTargetsEvent.KIND -> R.string.kind_pay_to PeopleListEvent.KIND -> R.string.kind_people_lists + ProfileBadgesEvent.KIND -> R.string.kind_profile_badges PictureEvent.KIND -> R.string.kind_pictures PinListEvent.KIND -> R.string.kind_pins ZapPollEvent.KIND -> R.string.kind_zap_poll diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index c4372a5f7..866522906 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1744,6 +1744,7 @@ Audio Track Badge Awards Badge Definitions + Accepted Badge Set Profile Badges Blocked Relays Blossom Servers diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/BadgeProfilesEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/AcceptedBadgeSetEvent.kt similarity index 94% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/BadgeProfilesEvent.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/AcceptedBadgeSetEvent.kt index b16598a3d..fe5f01e65 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/BadgeProfilesEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/AcceptedBadgeSetEvent.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.nip58Badges.profiles +package com.vitorpamplona.quartz.nip58Badges.accepted import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Address @@ -34,11 +34,11 @@ import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip58Badges.profiles.tags.AcceptedBadge +import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge import com.vitorpamplona.quartz.utils.TimeUtils @Immutable -class BadgeProfilesEvent( +class AcceptedBadgeSetEvent( id: HexKey, pubKey: HexKey, createdAt: Long, @@ -79,7 +79,7 @@ class BadgeProfilesEvent( fun build( acceptedBadges: List = emptyList(), createdAt: Long = TimeUtils.now(), - initializer: TagArrayBuilder.() -> Unit = {}, + initializer: TagArrayBuilder.() -> Unit = {}, ) = eventTemplate(KIND, "", createdAt) { dTag(STANDARD_D_TAG) alt(ALT_DESCRIPTION) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayBuilderExt.kt new file mode 100644 index 000000000..74e1d111a --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayBuilderExt.kt @@ -0,0 +1,26 @@ +/* + * 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.nip58Badges.accepted + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge + +fun TagArrayBuilder.acceptedBadges(badges: List) = addAll(AcceptedBadge.assemble(badges)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayExt.kt similarity index 92% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayExt.kt index 1574c11c3..b662d9df4 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/TagArrayExt.kt @@ -18,12 +18,12 @@ * 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.nip58Badges.profiles +package com.vitorpamplona.quartz.nip58Badges.accepted import com.vitorpamplona.quartz.nip01Core.core.TagArray import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents -import com.vitorpamplona.quartz.nip58Badges.profiles.tags.AcceptedBadge +import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge fun TagArray.acceptedBadges() = AcceptedBadge.parseAll(this) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/tags/AcceptedBadge.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/tags/AcceptedBadge.kt similarity index 97% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/tags/AcceptedBadge.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/tags/AcceptedBadge.kt index e0ed90e0f..fd7c08a01 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/tags/AcceptedBadge.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/accepted/tags/AcceptedBadge.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.nip58Badges.profiles.tags +package com.vitorpamplona.quartz.nip58Badges.accepted.tags import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.TagArray diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/ProfileBadgesEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/ProfileBadgesEvent.kt new file mode 100644 index 000000000..1680530b6 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/ProfileBadgesEvent.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.quartz.nip58Badges.profile + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.BaseReplaceableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +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.nip31Alts.alt +import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class ProfileBadgesEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseReplaceableEvent(id, pubKey, createdAt, KIND, tags, content, sig), + EventHintProvider, + AddressHintProvider, + PubKeyHintProvider { + override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint) + + override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey) + + override fun eventHints() = tags.mapNotNull(ETag::parseAsHint) + + override fun linkedEventIds() = tags.mapNotNull(ETag::parseId) + + override fun addressHints() = tags.mapNotNull(ATag::parseAsHint) + + override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId) + + fun acceptedBadges() = tags.acceptedBadges() + + fun badgeAwardEvents() = tags.badgeAwardEvents() + + fun badgeAwardDefinitions() = tags.badgeAwardDefinitions() + + companion object { + const val KIND = 10008 + const val ALT_DESCRIPTION = "List of accepted badges by the author" + + fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, "") + + fun createAddressTag(pubKey: HexKey): ATag = ATag(KIND, pubKey, "", null) + + fun build( + acceptedBadges: List = emptyList(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, "", createdAt) { + alt(ALT_DESCRIPTION) + if (acceptedBadges.isNotEmpty()) { + acceptedBadges(acceptedBadges) + } + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayBuilderExt.kt similarity index 87% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayBuilderExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayBuilderExt.kt index 459f04433..904cd211e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profiles/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayBuilderExt.kt @@ -18,9 +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.nip58Badges.profiles +package com.vitorpamplona.quartz.nip58Badges.profile import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -import com.vitorpamplona.quartz.nip58Badges.profiles.tags.AcceptedBadge +import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge -fun TagArrayBuilder.acceptedBadges(badges: List) = addAll(AcceptedBadge.assemble(badges)) +fun TagArrayBuilder.acceptedBadges(badges: List) = addAll(AcceptedBadge.assemble(badges)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayExt.kt new file mode 100644 index 000000000..478737e30 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/TagArrayExt.kt @@ -0,0 +1,32 @@ +/* + * 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.nip58Badges.profile + +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses +import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents +import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge + +fun TagArray.acceptedBadges() = AcceptedBadge.parseAll(this) + +fun TagArray.badgeAwardEvents() = taggedEvents() + +fun TagArray.badgeAwardDefinitions() = taggedAddresses() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/tags/AcceptedBadge.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/tags/AcceptedBadge.kt new file mode 100644 index 000000000..2d10142fa --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip58Badges/profile/tags/AcceptedBadge.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.quartz.nip58Badges.profile.tags + +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip01Core.core.TagArray +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag + +/** + * Represents a paired `a` (badge definition) and `e` (badge award) tag + * as specified in NIP-58 Profile Badges. + * + * Clients SHOULD ignore unpaired `a` or `e` tags. + */ +@Stable +class AcceptedBadge( + val badgeDefinition: ATag, + val badgeAward: ETag, +) { + companion object { + /** + * Parses ordered consecutive pairs of `a` and `e` tags from a tag array. + * Tags must appear as sequential pairs: `a`, `e`, `a`, `e`, ... + * Unpaired tags are ignored per the spec. + */ + fun parseAll(tags: TagArray): List { + val result = mutableListOf() + var i = 0 + while (i < tags.size - 1) { + val aTag = ATag.parse(tags[i]) + if (aTag != null) { + val eTag = ETag.parse(tags[i + 1]) + if (eTag != null) { + result.add(AcceptedBadge(aTag, eTag)) + i += 2 + continue + } + } + i++ + } + return result + } + + fun assemble(badges: List): List> = + badges.flatMap { badge -> + listOf( + badge.badgeDefinition.toATagArray(), + badge.badgeAward.toTagArray(), + ) + } + } +} 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 6c77feac1..06b51ad2e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -140,9 +140,10 @@ import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip58Badges.accepted.AcceptedBadgeSetEvent import com.vitorpamplona.quartz.nip58Badges.award.BadgeAwardEvent import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent -import com.vitorpamplona.quartz.nip58Badges.profiles.BadgeProfilesEvent +import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip5aStaticWebsites.NamedSiteEvent @@ -268,6 +269,7 @@ class EventFactory { sig: HexKey, ): T = when (kind) { + AcceptedBadgeSetEvent.KIND -> AcceptedBadgeSetEvent(id, pubKey, createdAt, tags, content, sig) AdvertisedRelayListEvent.KIND -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig) AppDefinitionEvent.KIND -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig) AppRecommendationEvent.KIND -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig) @@ -281,7 +283,6 @@ class EventFactory { AudioTrackEvent.KIND -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig) BadgeAwardEvent.KIND -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig) BadgeDefinitionEvent.KIND -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig) - BadgeProfilesEvent.KIND -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig) BidEvent.KIND -> BidEvent(id, pubKey, createdAt, tags, content, sig) BidConfirmationEvent.KIND -> BidConfirmationEvent(id, pubKey, createdAt, tags, content, sig) BlockedRelayListEvent.KIND -> BlockedRelayListEvent(id, pubKey, createdAt, tags, content, sig) @@ -439,6 +440,7 @@ class EventFactory { P2POrderEvent.KIND -> P2POrderEvent(id, pubKey, createdAt, tags, content, sig) PictureEvent.KIND -> PictureEvent(id, pubKey, createdAt, tags, content, sig) PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig) + ProfileBadgesEvent.KIND -> ProfileBadgesEvent(id, pubKey, createdAt, tags, content, sig) ZapPollEvent.KIND -> ZapPollEvent(id, pubKey, createdAt, tags, content, sig) PollEvent.KIND -> PollEvent(id, pubKey, createdAt, tags, content, sig) PollResponseEvent.KIND -> PollResponseEvent(id, pubKey, createdAt, tags, content, sig)