From 68f0ded7d5ebafc127b80056872ca62b4d45bdd8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 00:45:01 +0000 Subject: [PATCH 01/21] feat: pin followed public chats to the top of the Public Chats feed --- .../ui/screen/loggedIn/publicChats/PublicChatsScreen.kt | 4 +++- .../loggedIn/publicChats/dal/PublicChatsFeedFilter.kt | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt index ae177d56d9..ff2155f1ef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt @@ -106,8 +106,10 @@ fun WatchAccountForPublicChatsScreen( val hiddenUsers = accountViewModel.account.hiddenUsers.flow .collectAsStateWithLifecycle() + val followedChats by accountViewModel.account.publicChatList.flowSet + .collectAsStateWithLifecycle() - LaunchedEffect(accountViewModel, listState, hiddenUsers) { + LaunchedEffect(accountViewModel, listState, hiddenUsers, followedChats) { publicChatsFeedState.checkKeysInvalidateDataAndSendToTop() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt index f55e184e26..6932f2f88e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt @@ -32,7 +32,10 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel class PublicChatsFeedFilter( val account: Account, ) : AdditiveFeedFilter() { - override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code + override fun feedKey(): String = + account.userProfile().pubkeyHex + "-" + followList().code + "-" + + account.publicChatList.flowSet.value + .hashCode() override fun limit() = 200 @@ -105,6 +108,8 @@ class PublicChatsFeedFilter( } override fun sort(items: Set): List { + val followedSet = account.publicChatList.flowSet.value + val lastNote = items.associateWith { note -> LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNote?.createdAt() ?: 0L @@ -117,6 +122,8 @@ class PublicChatsFeedFilter( val comparator: Comparator = compareByDescending { + it.idHex in followedSet + }.thenByDescending { lastNote[it] }.thenByDescending { createdNote[it] From 3915e2b36f4dbf1d91e3fbf82c4eb6370a360d3e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 02:20:40 +0000 Subject: [PATCH 02/21] refactor: render pinned followed chats as a separate LazyColumn section Instead of sorting followed chats to the top of the existing feed (which hides them when the feed filter or TopFilter excludes them), render them in a dedicated items{} block above the rest of the feed and skip them in the main itemsIndexed{} block. This keeps the feed filter, feedKey, and screen watchers untouched. --- .../publicChats/PublicChatsFeedLoaded.kt | 59 +++++++++++++++---- .../loggedIn/publicChats/PublicChatsScreen.kt | 4 +- .../publicChats/dal/PublicChatsFeedFilter.kt | 9 +-- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt index 73872b432b..ce61cb95ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt @@ -23,14 +23,19 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -47,30 +52,58 @@ fun PublicChatsFeedLoaded( nav: INav, ) { val items by loaded.feed.collectAsStateWithLifecycle() + val followedSet by accountViewModel.account.publicChatList.flowSet + .collectAsStateWithLifecycle() + + val pinned = + remember(followedSet) { + followedSet.mapNotNull { idHex -> + LocalCache.getNoteIfExists(idHex)?.takeIf { it.event is ChannelCreateEvent } + } + } LazyColumn( contentPadding = rememberFeedContentPadding(FeedPadding), state = listState, ) { + items( + pinned, + key = { item -> "pinned-" + item.idHex }, + contentType = { item -> item.event?.kind ?: -1 }, + ) { item -> + PublicChatRow(item, accountViewModel, nav) + } + itemsIndexed( items.list, key = { _, item -> item.idHex }, contentType = { _, item -> item.event?.kind ?: -1 }, ) { _, item -> - Row(Modifier.fillMaxWidth().animateItem()) { - ChannelCardCompose( - baseNote = item, - routeForLastRead = "PublicChatsFeed", - modifier = Modifier.fillMaxWidth(), - forceEventKind = ChannelCreateEvent.KIND, - accountViewModel = accountViewModel, - nav = nav, - ) + if (item.idHex !in followedSet) { + PublicChatRow(item, accountViewModel, nav) } - - HorizontalDivider( - thickness = DividerThickness, - ) } } } + +@Composable +private fun LazyItemScope.PublicChatRow( + item: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + Row(Modifier.fillMaxWidth().animateItem()) { + ChannelCardCompose( + baseNote = item, + routeForLastRead = "PublicChatsFeed", + modifier = Modifier.fillMaxWidth(), + forceEventKind = ChannelCreateEvent.KIND, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + HorizontalDivider( + thickness = DividerThickness, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt index ff2155f1ef..ae177d56d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsScreen.kt @@ -106,10 +106,8 @@ fun WatchAccountForPublicChatsScreen( val hiddenUsers = accountViewModel.account.hiddenUsers.flow .collectAsStateWithLifecycle() - val followedChats by accountViewModel.account.publicChatList.flowSet - .collectAsStateWithLifecycle() - LaunchedEffect(accountViewModel, listState, hiddenUsers, followedChats) { + LaunchedEffect(accountViewModel, listState, hiddenUsers) { publicChatsFeedState.checkKeysInvalidateDataAndSendToTop() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt index 6932f2f88e..f55e184e26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/dal/PublicChatsFeedFilter.kt @@ -32,10 +32,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.base.IsInPublicChatChannel class PublicChatsFeedFilter( val account: Account, ) : AdditiveFeedFilter() { - override fun feedKey(): String = - account.userProfile().pubkeyHex + "-" + followList().code + "-" + - account.publicChatList.flowSet.value - .hashCode() + override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code override fun limit() = 200 @@ -108,8 +105,6 @@ class PublicChatsFeedFilter( } override fun sort(items: Set): List { - val followedSet = account.publicChatList.flowSet.value - val lastNote = items.associateWith { note -> LocalCache.getPublicChatChannelIfExists(note.idHex)?.lastNote?.createdAt() ?: 0L @@ -122,8 +117,6 @@ class PublicChatsFeedFilter( val comparator: Comparator = compareByDescending { - it.idHex in followedSet - }.thenByDescending { lastNote[it] }.thenByDescending { createdNote[it] From 4d4d81edbd3ae72b938213ef2687d214cf7325f2 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 21 May 2026 00:12:42 +0200 Subject: [PATCH 03/21] translate calendar + on-chain wallet strings to cs, de, pt-BR, svclose remaining translation gaps in cs, de, pt-BR, sv Translate the plurals and strings missed by the first pass --- .../src/main/res/values-cs-rCZ/strings.xml | 152 ++++++++++++++++++ .../src/main/res/values-de-rDE/strings.xml | 149 +++++++++++++++++ .../src/main/res/values-pt-rBR/strings.xml | 146 +++++++++++++++++ .../src/main/res/values-sv-rSE/strings.xml | 145 +++++++++++++++++ 4 files changed, 592 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 90ca003b07..ec275a1b81 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -2647,4 +2647,156 @@ Nowhere Zpráva Nowhere Umění Nowhere Fórum + + %1$d min + %1$d min + %1$d min + %1$d min + + Kalendáře + Celodenní + Pokračuje + (bez názvu) + Žádné události + V tento den nejsou žádné události + Skočit na dnešek + Další den + Další týden + Další měsíc + Předchozí den + Předchozí týden + Předchozí měsíc + Den + Týden + Měsíc + Feed + Minulé + Nadcházející + Z vybraného feedu zatím nejsou žádné nadcházející ani minulé události. + Váš kalendář je prázdný + Události sdílené lidmi, které sledujete, se zobrazí zde. Klepnutím na tlačítko + vytvořte vlastní. + Nic naplánováno + V tento den nejsou žádné události. Klepnutím na + přidejte. + Tento týden nic + V tomto týdnu nejsou žádné události. + Zatím žádné kalendářové kolekce. + Zatím žádné kolekce + Spojte události dohromady — sérii setkání, sekci konference, plán vašeho týmu. Klepnutím na + vytvořte novou. + vybráno + dnes + Den %1$d z %2$d + Zobrazit možnosti vytvoření + Vytvořit novou událost kalendáře + Vytvořit novou kalendářovou kolekci + Nový kalendář + Nová událost kalendáře + Upravit kalendář + Upravit událost kalendáře + Název + Popis + Název je povinný. + V tomto kalendáři zatím nejsou žádné události. + Zatím jste nevytvořili žádné události. + Události v tomto kalendáři (%1$d) + Smazat kalendář + Smazat tento kalendář? + Seznam kalendáře bude odstraněn. Události v něm nebudou smazány. + Název + Shrnutí + Místo + Začátek + Konec + Konec musí být po začátku. + Celodenní událost + Uzamčeno během úprav — změna by vytvořila novou událost. + Hashtagy (oddělené čárkou) + URL obrázku + Vybrat datum + Vybrat čas + Vybrat obrázek + Nahrání obrázku selhalo + Vybraný obrázek se nepodařilo nahrát. Zkuste to znovu nebo vložte URL. + Načítání události… + Název a začátek jsou povinné. + V kalendářích (%1$d) + Zatím není součástí žádného kalendáře. + Účastníci (%1$d) + Účastníci (%1$d) + Hledat jméno, npub nebo nip-05 + Zadejte platný npub… nebo 64znakový hex pubkey. + Odebrat účastníka + Všechny kalendáře + Zatím jste nevytvořili žádný kalendář. + Zobrazit události z… + Přidat do kalendáře + Přidat do jednoho z vašich kalendářů + Zatím jste nevytvořili žádný kalendář. + Přidat do kalendáře v telefonu + Sdílet jako nostr odkaz + Sdílet odkaz na kalendář + Exportovat do kalendáře (.ics) + Sdílet událost kalendáře + Otevřít v mapách + Otevřít odkaz + Probíhá nyní + %1$s · končí %2$s + RSVP (%1$d) + Účastním se + ✓ Účastním se + Možná + \? Možná + Nemůžu + ✗ Nemůžu + Zatím žádné RSVP. + calendar_reminders + Připomenutí kalendáře + Upozornění, když je událost, které se účastníte, blízko začátku. + Událost kalendáře + Připomenutí kalendáře + Posílat připomenutí + Oznámení se spustí, když je událost, které se účastníte, blízko začátku. + Doba předstihu připomenutí + Kolik minut před událostí chcete být upozorněni. + Kalendáře + Seznamy kalendářů + Detail události + On-chain Bitcoin zap + Čeká na potvrzení + Nová on-chain částka v satoshi + Rychlé částky on-chain zapů + On-chain zapy platí poplatky těžařům, takže částky jsou obvykle vyšší než u Lightningu. Klepnutím na částku ji odeberete. + Odeslat on-chain + Žádné transakce neodpovídají tomuto filtru + Zkopírovat adresu + Pro tento účet není k dispozici žádná on-chain adresa. + Není nakonfigurován žádný chain backend. + Čeká + Veřejné + Tato peněženka je veřejná + Vaše Taproot adresa je odvozena z vašeho veřejného klíče Nostr, takže kdokoli, kdo zná váš npub, může vidět zůstatek této peněženky a historii transakcí na blockchainu.\n\nAbyste zachovali soukromí, plňte a vybírejte tuto peněženku z neprivátních účtů, jako jsou burzy. Nikdy nemíchejte tyto prostředky se svými studenými peněženkami a zacházejte s nimi jako s penězi, které můžete ztratit, protože kdokoli, kdo má pod kontrolou vaše nsec, je může utratit. + Rozumím + On-chain transakce + Nowhere Drop + + %1$d událost + %1$d události + %1$d událostí + %1$d událostí + + + %1$s, žádné události + %1$s, %2$d událost + %1$s, %2$d události + %1$s, %2$d událostí + %1$s, %2$d událostí + + + Začíná za %1$d minutu + Začíná za %1$d minuty + Začíná za %1$d minut + Začíná za %1$d minut + + Seznam sledování + Seznam sledování (%1$d) + Seznam sledování obsahující %1$d uživatelů: diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 62f11d5819..c8de7d114b 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -2621,4 +2621,153 @@ anz der Bedingungen ist erforderlich Nowhere Shop Nowhere Nachricht Nowhere Kunst + + %1$d Min. + %1$d Min. + + Kalender + Ganztägig + Geht weiter + (ohne Titel) + Keine Termine + Keine Termine an diesem Tag + Zu heute springen + Nächster Tag + Nächste Woche + Nächster Monat + Vorheriger Tag + Vorherige Woche + Vorheriger Monat + Tag + Woche + Monat + Feed + Vergangen + Bevorstehend + Noch keine bevorstehenden oder vergangenen Kalendertermine aus deinem ausgewählten Feed. + Dein Kalender ist leer + Termine, die von Personen geteilt werden, denen du folgst, erscheinen hier. Tippe auf +, um eigene zu erstellen. + Nichts geplant + Keine Termine an diesem Tag. Tippe auf +, um einen hinzuzufügen. + Nichts diese Woche + Keine Termine in dieser Woche. + Noch keine Kalendersammlungen. + Noch keine Sammlungen + Gruppiere Termine — eine Meetup-Reihe, ein Konferenz-Track, die Roadmap deines Teams. Tippe auf +, um eine zu erstellen. + ausgewählt + heute + Tag %1$d von %2$d + Erstellungsoptionen anzeigen + Neuen Kalendertermin erstellen + Neue Kalendersammlung erstellen + Neuer Kalender + Neuer Kalendertermin + Kalender bearbeiten + Kalendertermin bearbeiten + Titel + Beschreibung + Ein Titel ist erforderlich. + Noch keine Termine in diesem Kalender. + Du hast noch keine Kalendertermine erstellt. + Termine in diesem Kalender (%1$d) + Kalender löschen + Diesen Kalender löschen? + Die Kalenderliste wird entfernt. Termine darin werden nicht gelöscht. + Titel + Zusammenfassung + Ort + Beginnt + Endet + Ende muss nach Beginn sein. + Ganztägiger Termin + Während der Bearbeitung gesperrt — eine Änderung würde stattdessen einen neuen Termin erstellen. + Hashtags (durch Komma getrennt) + Bild-URL + Datum wählen + Zeit wählen + Bild wählen + Bild-Upload fehlgeschlagen + Das gewählte Bild konnte nicht hochgeladen werden. Versuche es erneut oder füge eine URL ein. + Termin wird geladen… + Titel und Start sind erforderlich. + In Kalendern (%1$d) + Noch nicht Teil eines Kalenders. + Teilnehmer (%1$d) + Teilnehmer (%1$d) + Name, npub oder nip-05 suchen + Gib einen gültigen npub… oder 64-stelligen hex pubkey ein. + Teilnehmer entfernen + Alle Kalender + Du hast noch keine Kalender erstellt. + Termine anzeigen aus… + Zu einem Kalender hinzufügen + Zu einem deiner Kalender hinzufügen + Du hast noch keine Kalender erstellt. + Zum Telefonkalender hinzufügen + Als Nostr-Link teilen + Kalender-Link teilen + In Kalender exportieren (.ics) + Kalendertermin teilen + In Karten öffnen + Link öffnen + Findet jetzt statt + %1$s · endet %2$s + Zusagen (%1$d) + Komme + ✓ Komme + Vielleicht + \? Vielleicht + Kann nicht + ✗ Kann nicht + Noch keine Zusagen. + calendar_reminders + Kalendererinnerungen + Hinweis, wenn ein Termin, an dem du teilnimmst, bald beginnt. + Kalendertermin + Kalendererinnerungen + Erinnerungen senden + Eine Benachrichtigung wird angezeigt, wenn ein Termin, an dem du teilnimmst, bald beginnt. + Vorlaufzeit der Erinnerung + Wie viele Minuten vor dem Termin du benachrichtigt werden möchtest. + Kalender + Kalenderlisten + Termindetails + On-Chain-Bitcoin-Zap + Wartet auf Bestätigung + Neuer On-Chain-Betrag in Sats + Schnelle On-Chain-Zap-Beträge + On-Chain-Zaps zahlen Miner-Gebühren, daher sind die Beträge meist größer als bei Lightning. Tippe auf einen Betrag, um ihn zu entfernen. + Stattdessen on-chain senden + Keine Transaktionen entsprechen diesem Filter + Adresse kopieren + Keine On-Chain-Adresse für dieses Konto verfügbar. + Kein Chain-Backend konfiguriert. + Ausstehend + Öffentlich + Diese Wallet ist öffentlich + Deine Taproot-Adresse wird aus deinem öffentlichen Nostr-Schlüssel abgeleitet, daher kann jeder, der deinen npub kennt, den Saldo und die Transaktionshistorie dieser Wallet in der Blockchain einsehen.\n\nUm deine Privatsphäre zu wahren, befülle und entleere diese Wallet von und zu nicht-privaten Konten, wie Börsen. Vermische diese Mittel niemals mit deinen Cold Wallets und behandle sie wie Geld, das du verlieren kannst, da jeder, der die Kontrolle über deinen nsec hat, es ausgeben kann. + Verstanden + On-Chain-Transaktionen + Nowhere Drop + + %1$d Termin + %1$d Termine + + + %1$s, keine Termine + %1$s, %2$d Termin + %1$s, %2$d Termine + + + Beginnt in %1$d Minute + Beginnt in %1$d Minuten + + Folgenliste + Folgenliste (%1$d) + Folgenliste mit %1$d Nutzern: + Links + Issues + Nowhere Veranstaltung + Nowhere Forum + Nowhere Petition diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 7d60e1d9b4..04414f4d48 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -2619,4 +2619,150 @@ Nowhere Mensagem Nowhere Arte Nowhere Fórum + + %1$d min + %1$d min + + Calendários + Dia inteiro + Continua + (sem título) + Sem eventos + Sem eventos neste dia + Ir para hoje + Próximo dia + Próxima semana + Próximo mês + Dia anterior + Semana anterior + Mês anterior + Dia + Semana + Mês + Feed + Passados + Próximos + Nenhum evento de calendário futuro ou passado do feed selecionado ainda. + Seu calendário está vazio + Eventos compartilhados por pessoas que você segue aparecem aqui. Toque no botão + para criar os seus. + Nada agendado + Sem eventos neste dia. Toque em + para adicionar um. + Nada esta semana + Nenhum evento nesta semana. + Sem coleções de calendário ainda. + Sem coleções ainda + Agrupe eventos — uma série de encontros, uma trilha de conferência, o roteiro da sua equipe. Toque em + para criar uma. + selecionado + hoje + Dia %1$d de %2$d + Mostrar opções de criação + Criar um novo evento de calendário + Criar uma nova coleção de calendário + Novo calendário + Novo evento de calendário + Editar calendário + Editar evento de calendário + Título + Descrição + Um título é obrigatório. + Nenhum evento neste calendário ainda. + Você ainda não criou nenhum evento de calendário. + Eventos neste calendário (%1$d) + Excluir calendário + Excluir este calendário? + A lista do calendário será removida. Os eventos dentro dela não são excluídos. + Título + Resumo + Local + Começa + Termina + O término deve ser após o início. + Evento de dia inteiro + Bloqueado durante edição — alterar isto criaria um novo evento. + Hashtags (separadas por vírgula) + URL da imagem + Escolher data + Escolher hora + Escolher imagem + Falha no envio da imagem + A imagem escolhida não pôde ser enviada. Tente novamente ou cole uma URL. + Carregando evento… + Título e início são obrigatórios. + Em calendários (%1$d) + Ainda não faz parte de nenhum calendário. + Participantes (%1$d) + Participantes (%1$d) + Buscar nome, npub ou nip-05 + Insira um npub… válido ou pubkey hex de 64 caracteres. + Remover participante + Todos os calendários + Você ainda não criou nenhum calendário. + Mostrar eventos de… + Adicionar a um calendário + Adicionar a um dos seus calendários + Você ainda não criou nenhum calendário. + Adicionar ao calendário do telefone + Compartilhar como link nostr + Compartilhar link do calendário + Exportar para calendário (.ics) + Compartilhar evento de calendário + Abrir no mapa + Abrir link + Acontecendo agora + %1$s · termina %2$s + Confirmações (%1$d) + Vou + ✓ Vou + Talvez + \? Talvez + Não posso + ✗ Não posso + Sem confirmações ainda. + calendar_reminders + Lembretes de calendário + Aviso quando um evento do qual você participa está prestes a começar. + Evento de calendário + Lembretes de calendário + Enviar lembretes + Uma notificação é disparada quando um evento do qual você participa está prestes a começar. + Antecedência do lembrete + Quantos minutos antes do evento você quer ser notificado. + Calendários + Listas de calendário + Detalhes do evento + Zap Bitcoin on-chain + Aguardando confirmação + Novo valor on-chain em sats + Valores rápidos de zap on-chain + Zaps on-chain pagam taxas de mineradores, por isso os valores costumam ser maiores que no Lightning. Toque num valor para removê-lo. + Enviar on-chain em vez disso + Nenhuma transação corresponde a este filtro + Copiar endereço + Nenhum endereço on-chain disponível para esta conta. + Nenhum backend de chain configurado. + Pendente + Pública + Esta carteira é pública + Seu endereço Taproot é derivado da sua chave pública Nostr, então qualquer pessoa que conheça seu npub pode ver o saldo e o histórico de transações desta carteira na blockchain.\n\nPara preservar sua privacidade, abasteça e esvazie esta carteira a partir de e para contas não privadas, como corretoras. Nunca misture esses fundos com suas carteiras frias e trate-os como dinheiro que você pode perder, pois qualquer um que controle seu nsec pode gastá-lo. + Entendi + Transações on-chain + Nowhere Drop + + %1$d evento + %1$d eventos + + + %1$s, sem eventos + %1$s, %2$d evento + %1$s, %2$d eventos + + + Começa em %1$d minuto + Começa em %1$d minutos + + Lista de seguidos + Lista de seguidos (%1$d) + Lista de seguidos contendo %1$d usuário(s): + Links + Issues diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 9d46d84fb3..b3663a1f7f 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -2619,4 +2619,149 @@ Nowhere Namninsamling Nowhere Meddelande Nowhere Konst + + %1$d min + %1$d min + + Kalendrar + Heldag + Fortsätter + (utan titel) + Inga händelser + Inga händelser denna dag + Hoppa till idag + Nästa dag + Nästa vecka + Nästa månad + Föregående dag + Föregående vecka + Föregående månad + Dag + Vecka + Månad + Flöde + Tidigare + Kommande + Inga kommande eller tidigare kalenderhändelser från ditt valda flöde än. + Din kalender är tom + Händelser delade av personer du följer dyker upp här. Tryck på +-knappen för att skapa egna. + Inget schemalagt + Inga händelser denna dag. Tryck på + för att lägga till en. + Inget den här veckan + Inga händelser denna vecka. + Inga kalendersamlingar än. + Inga samlingar än + Gruppera händelser tillsammans — en träffserie, ett konferensspår, ditt teams färdplan. Tryck på + för att skapa en. + vald + idag + Dag %1$d av %2$d + Visa skapandealternativ + Skapa en ny kalenderhändelse + Skapa en ny kalendersamling + Ny kalender + Ny kalenderhändelse + Redigera kalender + Redigera kalenderhändelse + Titel + Beskrivning + En titel krävs. + Inga händelser i denna kalender än. + Du har inte skapat några kalenderhändelser än. + Händelser i denna kalender (%1$d) + Radera kalender + Radera denna kalender? + Kalenderlistan kommer att tas bort. Händelser inuti den raderas inte. + Titel + Sammanfattning + Plats + Börjar + Slutar + Slut måste vara efter start. + Heldagshändelse + Låst under redigering — att ändra detta skulle skapa en ny händelse istället. + Hashtaggar (kommaseparerade) + Bild-URL + Välj datum + Välj tid + Välj bild + Bilduppladdning misslyckades + Den valda bilden kunde inte laddas upp. Försök igen eller klistra in en URL. + Laddar händelse… + Titel och starttid krävs. + I kalendrar (%1$d) + Inte del av någon kalender än. + Deltagare (%1$d) + Deltagare (%1$d) + Sök namn, npub eller nip-05 + Ange en giltig npub… eller 64-teckens hex pubkey. + Ta bort deltagare + Alla kalendrar + Du har inte skapat några kalendrar än. + Visa händelser från… + Lägg till i en kalender + Lägg till i en av dina kalendrar + Du har inte skapat några kalendrar än. + Lägg till i telefonens kalender + Dela som nostr-länk + Dela kalenderlänk + Exportera till kalender (.ics) + Dela kalenderhändelse + Öppna i kartor + Öppna länk + Pågår nu + %1$s · slutar %2$s + OSA (%1$d) + Kommer + ✓ Kommer + Kanske + \? Kanske + Kan inte + ✗ Kan inte + Inga OSA än. + calendar_reminders + Kalenderpåminnelser + Påminnelse när en händelse du deltar i snart ska börja. + Kalenderhändelse + Kalenderpåminnelser + Skicka påminnelser + En notis visas när en händelse du deltar i snart ska börja. + Påminnelsetid + Hur många minuter före händelsen du vill bli notifierad. + Kalendrar + Kalenderlistor + Händelsedetaljer + On-chain Bitcoin-zap + Väntar på bekräftelse + Nytt on-chain-belopp i sats + Snabba on-chain-zapbelopp + On-chain-zappar betalar miner-avgifter, så beloppen är vanligtvis större än Lightning. Tryck på ett belopp för att ta bort det. + Skicka on-chain istället + Inga transaktioner matchar detta filter + Kopiera adress + Ingen on-chain-adress tillgänglig för detta konto. + Ingen chain-backend är konfigurerad. + Väntar + Offentlig + Denna plånbok är offentlig + Din Taproot-adress härleds från din publika Nostr-nyckel, så alla som känner till din npub kan se denna plånboks saldo och transaktionshistorik på blockchain.\n\nFör att bevara din integritet, fyll på och töm denna plånbok från och till icke-privata konton, som börser. Blanda aldrig dessa medel med dina kalla plånböcker, och behandla dem som pengar du kan förlora, eftersom alla som har kontroll över din nsec kan spendera dem. + Uppfattat + On-chain-transaktioner + Nowhere Drop + + %1$d händelse + %1$d händelser + + + %1$s, inga händelser + %1$s, %2$d händelse + %1$s, %2$d händelser + + + Börjar om %1$d minut + Börjar om %1$d minuter + + Följlista + Följlista (%1$d) + Följlista som innehåller %1$d användare: + Nowhere Forum From 898dec73b95a938044e9b3c87a0271cf17e42934 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 21 May 2026 08:14:31 +0200 Subject: [PATCH 04/21] i18n: special-case count=0 at call site for calendar day a11y label --- .../loggedIn/calendars/CalendarDayA11y.kt | 40 +++++++++++++++++++ .../loggedIn/calendars/CalendarMonthView.kt | 4 +- .../loggedIn/calendars/CalendarWeekView.kt | 3 +- .../src/main/res/values-cs-rCZ/strings.xml | 2 +- .../src/main/res/values-de-rDE/strings.xml | 2 +- .../src/main/res/values-pt-rBR/strings.xml | 2 +- .../src/main/res/values-sv-rSE/strings.xml | 2 +- amethyst/src/main/res/values/strings.xml | 2 +- 8 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayA11y.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayA11y.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayA11y.kt new file mode 100644 index 0000000000..dcaec39af4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarDayA11y.kt @@ -0,0 +1,40 @@ +/* + * 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.calendars + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.pluralStringResource +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes + +// Picks calendar_day_a11y_no_events when count is 0 because ICU/CLDR maps 0 to +// the `other` category for every locale we ship, which would otherwise render +// "[date], 0 events" instead of "[date], no events". +@Composable +fun calendarDayA11yLabel( + dateLabel: String, + count: Int, +): String = + if (count == 0) { + stringRes(R.string.calendar_day_a11y_no_events, dateLabel) + } else { + pluralStringResource(R.plurals.calendar_day_a11y_events, count, dateLabel, count) + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt index 648210bee6..69322f2b74 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarMonthView.kt @@ -46,7 +46,6 @@ 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.res.pluralStringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -266,8 +265,7 @@ private fun DayCell( MaterialTheme.colorScheme.surface } - val baseDescription = - pluralStringResource(R.plurals.calendar_day_a11y_events, totalEventCount, dateLabel, totalEventCount) + val baseDescription = calendarDayA11yLabel(dateLabel, totalEventCount) val todaySuffix = stringRes(R.string.calendar_day_a11y_today_suffix) val selectedSuffix = stringRes(R.string.calendar_day_a11y_selected_suffix) val a11y = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt index c71f126c2a..46b9d8b4d9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/calendars/CalendarWeekView.kt @@ -44,7 +44,6 @@ 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.res.pluralStringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -202,7 +201,7 @@ private fun WeekStrip( } val dateLabel = formatLongDate(date.atStartOfDay(ZoneId.systemDefault()).toEpochSecond()) - val baseA11y = pluralStringResource(R.plurals.calendar_day_a11y_events, count, dateLabel, count) + val baseA11y = calendarDayA11yLabel(dateLabel, count) val todaySuffix = stringRes(R.string.calendar_day_a11y_today_suffix) val selectedSuffix = stringRes(R.string.calendar_day_a11y_selected_suffix) val a11y = diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index ec275a1b81..6cc5303a6d 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -2784,12 +2784,12 @@ %1$d událostí - %1$s, žádné události %1$s, %2$d událost %1$s, %2$d události %1$s, %2$d událostí %1$s, %2$d událostí + %1$s, žádné události Začíná za %1$d minutu Začíná za %1$d minuty diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index c8de7d114b..36fa2c5134 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -2754,10 +2754,10 @@ anz der Bedingungen ist erforderlich %1$d Termine - %1$s, keine Termine %1$s, %2$d Termin %1$s, %2$d Termine + %1$s, keine Termine Beginnt in %1$d Minute Beginnt in %1$d Minuten diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 04414f4d48..febd5b47b3 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -2752,10 +2752,10 @@ %1$d eventos - %1$s, sem eventos %1$s, %2$d evento %1$s, %2$d eventos + %1$s, sem eventos Começa em %1$d minuto Começa em %1$d minutos diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index b3663a1f7f..dd64ca4dc2 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -2752,10 +2752,10 @@ %1$d händelser - %1$s, inga händelser %1$s, %2$d händelse %1$s, %2$d händelser + %1$s, inga händelser Börjar om %1$d minut Börjar om %1$d minuter diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 557cfb208c..15b09c7920 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1932,10 +1932,10 @@ Add to phone calendar Jump to today - %1$s, no events %1$s, %2$d event %1$s, %2$d events + %1$s, no events today selected Create a new calendar event From 2d3a2582ee38bfd68e3b8e2f0028b7ef4c32b34c Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 21 May 2026 08:32:19 +0200 Subject: [PATCH 05/21] docs(skill): catch dead quantity="zero" entries in find-missing-translations --- .../skills/find-missing-translations/SKILL.md | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/.claude/skills/find-missing-translations/SKILL.md b/.claude/skills/find-missing-translations/SKILL.md index 9f8c14109c..f9bc30f72a 100644 --- a/.claude/skills/find-missing-translations/SKILL.md +++ b/.claude/skills/find-missing-translations/SKILL.md @@ -74,7 +74,14 @@ Before presenting results, **scan the missing English strings** for two red-flag 1. **Hardcoded `"1"` next to a noun.** A new English string like `"1 reply"`, `"1 follower"`, or `"1 minute ago"` almost always belongs in a `` resource — not a ``. Hardcoding `1` in English forces every translator to either also hardcode `1` (breaking languages where the `one` category covers other numbers, e.g. some Slavic languages) or to silently change the meaning. 2. **A `%d` / `%1$d` placeholder in a clearly singular/plural sentence** (e.g. `"%1$d reply"`, `"%d follower"`). Even though the placeholder is parameterised, English-only `one`/`other` agreement won't survive translation into languages that need `few`/`many`. -Also **audit existing `` resources** for the same anti-pattern — any locale's `quantity="one"` item that hardcodes the literal `1` (instead of using a `%d` / `%1$d` placeholder) is broken for languages where the `one` CLDR category covers more than just `n=1` (Russian, Ukrainian, Croatian, etc.). Flag and offer to fix: +Also **audit existing `` resources** for two anti-patterns: + +1. **`quantity="one"` items that hardcode the literal `1`** (instead of using a `%d` / `%1$d` placeholder) — broken for languages where the `one` CLDR category covers more than just `n=1` (Russian, Ukrainian, Croatian, etc.). +2. **`quantity="zero"` items in any locale that doesn't natively use the `zero` CLDR category** — i.e. **everything except Arabic (`ar`) and Welsh (`cy`)**. ICU/CLDR maps `count=0` to `other` for English and all the locales we ship to (cs, de, pt-BR, sv, etc.), so `` is **dead code** there: `getQuantityString(id, 0)` will pick `other`, never the zero entry, and the visible runtime string ends up `"…0 items"` instead of the intended `"…no items"`. + +If a UX genuinely wants special "no items" wording at count=0, that has to be a call-site `if (count == 0)` branch to a separate ``, **not** a `quantity="zero"` plural item. + +Flag and offer to fix: ```bash # Scan every locale's strings.xml for entries that @@ -96,6 +103,27 @@ for f in amethyst/src/main/res/values/strings.xml amethyst/src/main/res/values-* done ``` +Then scan for dead `quantity="zero"` entries. CLDR's `zero` category is integer-bearing only in **Arabic (`ar`)** and **Welsh (`cy`)**. In every other locale, count=0 falls through to `other`, so a `` entry is dead and likely a translator/author bug (or it silently never fires): + +```bash +for f in amethyst/src/main/res/values/strings.xml amethyst/src/main/res/values-*/strings.xml; do + # Skip Arabic and Welsh — they natively use the zero category. + case "$f" in + *values-ar*|*values-cy*) continue ;; + esac + awk -v file="$f" ' + /]*>/, "", text); sub(/<.*$/, "", text) + print file ": zero=\"" text "\"" + } + /<\/plurals>/ { in_plurals = 0 } + ' "$f" +done +``` + +For each hit, warn the user that the entry is unreachable in that locale. The fix is to **remove the ``** and, if the UX wanted distinct wording for count=0, add a separate `` plus an `if (count == 0)` branch at the call site (see "Plurals: handle with care" below). + Quick scan over the missing keys: ```bash @@ -147,6 +175,14 @@ When adding or proposing **``** entries, follow these rules: - Arabic (`ar`): `zero`, `one`, `two`, `few`, `many`, `other` - German / Swedish / Brazilian Portuguese: `one`, `other` - When a missing string contains a count placeholder and is conceptually a singular/plural pair, **flag it before translating** — it may belong as a `` resource rather than a single ``. Surface this to the user before proposing translations. +- **Do not use `quantity="zero"` outside Arabic (`ar`) and Welsh (`cy`).** CLDR's `zero` category is integer-bearing only in those two languages. Android calls `PluralRules.select(0)` for the device locale; in English/German/Czech/Polish/Russian/Swedish/Portuguese/etc. it returns `other`, so the explicit `` is never picked at runtime and the user sees `"…0 items"` instead of the intended wording. If the design calls for "no items" at count=0, model it as a separate `` and an `if (count == 0)` branch at the call site: + ```kotlin + val label = if (count == 0) { + stringRes(R.string.foo_no_items, dateLabel) + } else { + pluralStringResource(R.plurals.foo_items, count, dateLabel, count) + } + ``` - Reference: [Android `` docs](https://developer.android.com/guide/topics/resources/string-resource#Plurals) and [CLDR plural rules](https://unicode-org.github.io/cldr-staging/charts/latest/supplemental/language_plural_rules.html). **Then ask the user:** "Would you like me to translate these missing strings into [list of target locales]?" @@ -165,4 +201,5 @@ When adding translated strings to locale files: - **Diffing each locale separately** — only diff against `cs-rCZ`; assume the same keys are missing everywhere - **Inserting strings in a specific position** — always append at the bottom; ordering is handled separately - **Hardcoding `"1"` in a `` `quantity="one"` item** — always use the count placeholder; otherwise non-English `one` categories produce wrong text -- **Copying English's `one`/`other` set into every locale** — each language must include all CLDR plural categories it uses (e.g. Czech needs `one`, `few`, `many`, `other`) \ No newline at end of file +- **Copying English's `one`/`other` set into every locale** — each language must include all CLDR plural categories it uses (e.g. Czech needs `one`, `few`, `many`, `other`) +- **Using `` to special-case count=0** — outside Arabic and Welsh, this entry is unreachable: ICU/CLDR maps 0 → `other`, so the runtime never picks the zero item and the user sees `"…0 items"`. Special-case at the call site with a separate `` instead. \ No newline at end of file From a877ef7ec7db8545c3c156f3d9109dd0861171f5 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 21 May 2026 09:19:12 +0200 Subject: [PATCH 06/21] i18n(cs): apply Czech translation review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native-speaker review surfaced several consistency and wording fixes in the Czech translations from this branch: - Use "Kanál" for "feed" (matches existing feed/home_feed/global_feed). - Use "sats" not "satoshi" (matches new_amount_in_sats / amount_in_sats). - Match Lightning sibling string's "odstraníte" (vs "odeberete") on quick_zap_amounts_onchain_explainer. - Capitalize "Nostr" in calendar_share_nostr. - Calendar collection naming: use "kalendář" for actions (New / Edit / Delete) matching the English source's user-facing simplification; keep the disambiguating "seznamy" wording for the empty-state body and title (matches route_calendar_collections = "Seznamy kalendářů" and English's "No calendar collections yet"). - Rephrase contact_list_containing_label to dodge Czech count inflection ("Seznam sledování (%1$d):" rather than "obsahující %1$d uživatelů" which is only correct for n ≥ 5). - Wallet privacy dialog: "vkládejte do peněženky a vybírejte z ní" (deposit/withdraw verbs) over "plňte a vybírejte"; "váš nsec" (masc. inanimate) over "vaše nsec"; "nesoukromých účtů" over "neprivátních". - "on-chain backend" over "chain backend" in wallet_onchain_no_backend. --- .../src/main/res/values-cs-rCZ/strings.xml | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 6cc5303a6d..3e488ce306 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -2669,25 +2669,25 @@ Den Týden Měsíc - Feed + Kanál Minulé Nadcházející - Z vybraného feedu zatím nejsou žádné nadcházející ani minulé události. + Z vybraného kanálu zatím nejsou žádné nadcházející ani minulé události. Váš kalendář je prázdný Události sdílené lidmi, které sledujete, se zobrazí zde. Klepnutím na tlačítko + vytvořte vlastní. Nic naplánováno V tento den nejsou žádné události. Klepnutím na + přidejte. Tento týden nic V tomto týdnu nejsou žádné události. - Zatím žádné kalendářové kolekce. - Zatím žádné kolekce - Spojte události dohromady — sérii setkání, sekci konference, plán vašeho týmu. Klepnutím na + vytvořte novou. + Zatím žádné kalendářové seznamy. + Zatím žádné seznamy + Spojte události dohromady — sérii setkání, sekci konference, plán vašeho týmu. Klepnutím na + vytvořte nový. vybráno dnes Den %1$d z %2$d Zobrazit možnosti vytvoření Vytvořit novou událost kalendáře - Vytvořit novou kalendářovou kolekci + Vytvořit nový kalendář Nový kalendář Nová událost kalendáře Upravit kalendář @@ -2700,7 +2700,7 @@ Události v tomto kalendáři (%1$d) Smazat kalendář Smazat tento kalendář? - Seznam kalendáře bude odstraněn. Události v něm nebudou smazány. + Kalendář bude odstraněn. Události v něm nebudou smazány. Název Shrnutí Místo @@ -2732,7 +2732,7 @@ Přidat do jednoho z vašich kalendářů Zatím jste nevytvořili žádný kalendář. Přidat do kalendáře v telefonu - Sdílet jako nostr odkaz + Sdílet jako Nostr odkaz Sdílet odkaz na kalendář Exportovat do kalendáře (.ics) Sdílet událost kalendáře @@ -2762,18 +2762,18 @@ Detail události On-chain Bitcoin zap Čeká na potvrzení - Nová on-chain částka v satoshi + Nová on-chain částka v sats Rychlé částky on-chain zapů - On-chain zapy platí poplatky těžařům, takže částky jsou obvykle vyšší než u Lightningu. Klepnutím na částku ji odeberete. + On-chain zapy platí poplatky těžařům, takže částky jsou obvykle vyšší než u Lightningu. Klepnutím na částku ji odstraníte. Odeslat on-chain Žádné transakce neodpovídají tomuto filtru Zkopírovat adresu Pro tento účet není k dispozici žádná on-chain adresa. - Není nakonfigurován žádný chain backend. + Není nakonfigurován žádný on-chain backend. Čeká Veřejné Tato peněženka je veřejná - Vaše Taproot adresa je odvozena z vašeho veřejného klíče Nostr, takže kdokoli, kdo zná váš npub, může vidět zůstatek této peněženky a historii transakcí na blockchainu.\n\nAbyste zachovali soukromí, plňte a vybírejte tuto peněženku z neprivátních účtů, jako jsou burzy. Nikdy nemíchejte tyto prostředky se svými studenými peněženkami a zacházejte s nimi jako s penězi, které můžete ztratit, protože kdokoli, kdo má pod kontrolou vaše nsec, je může utratit. + Vaše Taproot adresa je odvozena z vašeho veřejného klíče Nostr, takže kdokoli, kdo zná váš npub, může vidět zůstatek této peněženky a historii transakcí na blockchainu.\n\nAbyste zachovali soukromí, vkládejte do této peněženky a vybírejte z ní prostředky z nesoukromých účtů, jako jsou burzy. Nikdy nemíchejte tyto prostředky se svými studenými peněženkami a zacházejte s nimi jako s penězi, které můžete ztratit, protože kdokoli, kdo má pod kontrolou váš nsec, je může utratit. Rozumím On-chain transakce Nowhere Drop @@ -2798,5 +2798,5 @@ Seznam sledování Seznam sledování (%1$d) - Seznam sledování obsahující %1$d uživatelů: + Seznam sledování (%1$d): From db8791f5fc24d142df6b90296844c869cc99af22 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 21 May 2026 09:45:09 +0200 Subject: [PATCH 07/21] i18n(pt-BR): apply Brazilian Portuguese translation review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native-speaker review surfaced consistency and wording fixes: - Capitalize "Nostr" in calendar_share_nostr. - "no blockchain" (masculine, matches ots_info_description) over "na blockchain" in wallet_onchain_public_dialog_body. - Calendar collection naming: use "calendário" for action labels (New / Edit / Delete) matching the English source's user-facing simplification; use "lista de calendário" only in the empty-state body and route header (matches route_calendar_collections = "Listas de calendário" and English's "Calendar lists"). Same hybrid we resolved for Czech. - Symmetric RSVP pair: "Vou" / "Talvez" / "Não vou" (instead of mixed "Vou" / "Talvez" / "Não posso"). - "RSVPs" over "Confirmações" for calendar_rsvp_section and calendar_rsvp_none — the section also includes "Talvez" / "Não vou" responses, so "Confirmações" was misleading. Aligns with the existing "RSVP do Compromisso" usage in kind_appt_rsvp. - wallet_onchain_public_dialog_body: clearer privacy wording ("abasteça a partir de contas não privadas e envie os fundos de volta" over "abasteça e esvazie a partir de e para"); make antecedent of "gastar" explicit ("gastar os fundos" over ambiguous "-lo"). - calendar_empty_feed: reorder to drop trailing "ainda" — "Ainda não há eventos…" reads more naturally than "…ainda" at end. - calendar_collection_delete_confirm_message: align tenses (future + future "será removido / não serão excluídos" over future + present). - "backend on-chain" over "backend de chain" in wallet_onchain_no_backend. Note: contact_list_* strings use the semantically-correct "seguidos" (people you follow, per kind:3), even though existing follow_list_selection / follow_sets use the inverted "seguidores". Recommended follow-up: clean up legacy strings to match. --- .../src/main/res/values-pt-rBR/strings.xml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index febd5b47b3..27b2e99712 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -2642,22 +2642,22 @@ Feed Passados Próximos - Nenhum evento de calendário futuro ou passado do feed selecionado ainda. + Ainda não há eventos de calendário (futuros ou passados) do feed selecionado. Seu calendário está vazio Eventos compartilhados por pessoas que você segue aparecem aqui. Toque no botão + para criar os seus. Nada agendado Sem eventos neste dia. Toque em + para adicionar um. Nada esta semana Nenhum evento nesta semana. - Sem coleções de calendário ainda. - Sem coleções ainda + Ainda não há listas de calendário. + Ainda não há listas Agrupe eventos — uma série de encontros, uma trilha de conferência, o roteiro da sua equipe. Toque em + para criar uma. selecionado hoje Dia %1$d de %2$d Mostrar opções de criação Criar um novo evento de calendário - Criar uma nova coleção de calendário + Criar um novo calendário Novo calendário Novo evento de calendário Editar calendário @@ -2670,7 +2670,7 @@ Eventos neste calendário (%1$d) Excluir calendário Excluir este calendário? - A lista do calendário será removida. Os eventos dentro dela não são excluídos. + O calendário será removido. Os eventos dentro dele não serão excluídos. Título Resumo Local @@ -2702,7 +2702,7 @@ Adicionar a um dos seus calendários Você ainda não criou nenhum calendário. Adicionar ao calendário do telefone - Compartilhar como link nostr + Compartilhar como link Nostr Compartilhar link do calendário Exportar para calendário (.ics) Compartilhar evento de calendário @@ -2710,14 +2710,14 @@ Abrir link Acontecendo agora %1$s · termina %2$s - Confirmações (%1$d) + RSVPs (%1$d) Vou ✓ Vou Talvez \? Talvez - Não posso - ✗ Não posso - Sem confirmações ainda. + Não vou + ✗ Não vou + Sem RSVPs ainda. calendar_reminders Lembretes de calendário Aviso quando um evento do qual você participa está prestes a começar. @@ -2739,11 +2739,11 @@ Nenhuma transação corresponde a este filtro Copiar endereço Nenhum endereço on-chain disponível para esta conta. - Nenhum backend de chain configurado. + Nenhum backend on-chain configurado. Pendente Pública Esta carteira é pública - Seu endereço Taproot é derivado da sua chave pública Nostr, então qualquer pessoa que conheça seu npub pode ver o saldo e o histórico de transações desta carteira na blockchain.\n\nPara preservar sua privacidade, abasteça e esvazie esta carteira a partir de e para contas não privadas, como corretoras. Nunca misture esses fundos com suas carteiras frias e trate-os como dinheiro que você pode perder, pois qualquer um que controle seu nsec pode gastá-lo. + Seu endereço Taproot é derivado da sua chave pública Nostr, então qualquer pessoa que conheça seu npub pode ver o saldo e o histórico de transações desta carteira no blockchain.\n\nPara preservar sua privacidade, abasteça esta carteira a partir de contas não privadas (como corretoras) e envie os fundos de volta para elas. Nunca misture esses fundos com suas carteiras frias e trate-os como dinheiro que você pode perder, pois qualquer um que controle seu nsec pode gastar os fundos. Entendi Transações on-chain Nowhere Drop From 44f8d1bfa85b49fd3b525f0b7eca1e4a58d04c9c Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 21 May 2026 09:50:09 +0200 Subject: [PATCH 08/21] i18n(de): apply German translation review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native-speaker review surfaced consistency and wording fixes: - Calendar collection naming: use "Kalender" for action labels (New / Edit / Delete) matching the English source's user-facing simplification; use "Kalenderliste" only in the empty-state body and route header (matches route_calendar_collections = "Kalenderlisten" and English's "Calendar lists"). Same hybrid as Czech and pt-BR. - On-Chain hyphenation: "On-Chain-Backend" over "Chain-Backend" in wallet_onchain_no_backend; "Stattdessen On-Chain senden" over "Stattdessen on-chain senden" in send_onchain_instead. Matches the rest of the batch's German compound-noun convention. - "Nowhere Event" over "Nowhere Veranstaltung" — aligns with the English source's all-English Nowhere brand prefixes (Drop / Forum / Petition were already English). - "RSVPs" / "Antworten" over "Zusagen" for calendar_rsvp_section and calendar_rsvp_none — section also includes "Vielleicht" and "Komme nicht", so "Zusagen" was misleading. Aligns with the existing "RSVP für Termin" usage in kind_appt_rsvp. - Symmetric RSVP pair: "Komme" / "Vielleicht" / "Komme nicht" (instead of mixed "Komme" / "Vielleicht" / "Kann nicht"). - wallet_onchain_public_dialog_body: clearer privacy wording ("lade auf und sende zurück" over "befülle und entleere von und zu"); make antecedent of "ausgeben" explicit ("die Mittel ausgeben" over ambiguous "es"). - contact_list_containing_label: rephrase to dodge German declension for n=1 ("Folgenliste (%1$d Nutzer):" over "Folgenliste mit %1$d Nutzern:" which is dative-plural-only). --- .../src/main/res/values-de-rDE/strings.xml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 36fa2c5134..e2c1b6d217 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -2651,15 +2651,15 @@ anz der Bedingungen ist erforderlich Keine Termine an diesem Tag. Tippe auf +, um einen hinzuzufügen. Nichts diese Woche Keine Termine in dieser Woche. - Noch keine Kalendersammlungen. - Noch keine Sammlungen + Noch keine Kalenderlisten. + Noch keine Listen Gruppiere Termine — eine Meetup-Reihe, ein Konferenz-Track, die Roadmap deines Teams. Tippe auf +, um eine zu erstellen. ausgewählt heute Tag %1$d von %2$d Erstellungsoptionen anzeigen Neuen Kalendertermin erstellen - Neue Kalendersammlung erstellen + Neuen Kalender erstellen Neuer Kalender Neuer Kalendertermin Kalender bearbeiten @@ -2712,14 +2712,14 @@ anz der Bedingungen ist erforderlich Link öffnen Findet jetzt statt %1$s · endet %2$s - Zusagen (%1$d) + RSVPs (%1$d) Komme ✓ Komme Vielleicht \? Vielleicht - Kann nicht - ✗ Kann nicht - Noch keine Zusagen. + Komme nicht + ✗ Komme nicht + Noch keine Antworten. calendar_reminders Kalendererinnerungen Hinweis, wenn ein Termin, an dem du teilnimmst, bald beginnt. @@ -2737,15 +2737,15 @@ anz der Bedingungen ist erforderlich Neuer On-Chain-Betrag in Sats Schnelle On-Chain-Zap-Beträge On-Chain-Zaps zahlen Miner-Gebühren, daher sind die Beträge meist größer als bei Lightning. Tippe auf einen Betrag, um ihn zu entfernen. - Stattdessen on-chain senden + Stattdessen On-Chain senden Keine Transaktionen entsprechen diesem Filter Adresse kopieren Keine On-Chain-Adresse für dieses Konto verfügbar. - Kein Chain-Backend konfiguriert. + Kein On-Chain-Backend konfiguriert. Ausstehend Öffentlich Diese Wallet ist öffentlich - Deine Taproot-Adresse wird aus deinem öffentlichen Nostr-Schlüssel abgeleitet, daher kann jeder, der deinen npub kennt, den Saldo und die Transaktionshistorie dieser Wallet in der Blockchain einsehen.\n\nUm deine Privatsphäre zu wahren, befülle und entleere diese Wallet von und zu nicht-privaten Konten, wie Börsen. Vermische diese Mittel niemals mit deinen Cold Wallets und behandle sie wie Geld, das du verlieren kannst, da jeder, der die Kontrolle über deinen nsec hat, es ausgeben kann. + Deine Taproot-Adresse wird aus deinem öffentlichen Nostr-Schlüssel abgeleitet, daher kann jeder, der deinen npub kennt, den Saldo und die Transaktionshistorie dieser Wallet in der Blockchain einsehen.\n\nUm deine Privatsphäre zu wahren, lade diese Wallet von nicht-privaten Konten (z. B. Börsen) auf und sende Mittel auch nur an solche Konten zurück. Vermische diese Mittel niemals mit deinen Cold Wallets und behandle sie wie Geld, das du verlieren kannst, da jeder, der die Kontrolle über deinen nsec hat, die Mittel ausgeben kann. Verstanden On-Chain-Transaktionen Nowhere Drop @@ -2764,10 +2764,10 @@ anz der Bedingungen ist erforderlich Folgenliste Folgenliste (%1$d) - Folgenliste mit %1$d Nutzern: + Folgenliste (%1$d Nutzer): Links Issues - Nowhere Veranstaltung + Nowhere Event Nowhere Forum Nowhere Petition From ec2f621772231033b103421d2c2c52570f48610e Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 21 May 2026 09:52:51 +0200 Subject: [PATCH 09/21] i18n(sv): apply Swedish translation review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native-speaker review surfaced consistency and wording fixes: - Calendar collection naming: use "kalender" for action labels (New / Edit / Delete) matching the English source's user-facing simplification; use "kalenderlista" only in the empty-state body and route header (matches route_calendar_collections = "Kalenderlistor" and English's "Calendar lists"). Same hybrid as Czech, pt-BR, and German. - Capitalize "Nostr" in calendar_share_nostr. - "on-chain-backend" over "chain-backend" in wallet_onchain_no_backend (rest of batch already uses the on-chain- compound). - "blockchainen" (definite form, matches existing ots_info_description) over bare "blockchain" in wallet_onchain_public_dialog_body. - Tighten Lightning comparison: "större än för Lightning" over "större än Lightning" (the bare comparison was elliptical). - wallet_onchain_public_dialog_body: clearer privacy wording ("fyll på från icke-privata konton och skicka tillbaka medel dit" over "fyll på och töm från och till"); make antecedent of "spendera" explicit ("spendera medlen" over ambiguous "spendera dem"). - Symmetric RSVP pair: "Kommer" / "Kanske" / "Kommer inte" (instead of mixed "Kommer" / "Kanske" / "Kan inte"). Matches the same fix applied for German and pt-BR. Note: calendar_rsvp_section / calendar_rsvp_none already use the established Swedish "OSA" acronym (matching kind_appt_rsvp = "OSA för möte"), so no rename needed unlike de/pt-BR where the section noun ("Zusagen"/"Confirmações") had to be replaced. --- .../src/main/res/values-sv-rSE/strings.xml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index dd64ca4dc2..d467b17c42 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -2649,15 +2649,15 @@ Inga händelser denna dag. Tryck på + för att lägga till en. Inget den här veckan Inga händelser denna vecka. - Inga kalendersamlingar än. - Inga samlingar än + Inga kalenderlistor än. + Inga listor än Gruppera händelser tillsammans — en träffserie, ett konferensspår, ditt teams färdplan. Tryck på + för att skapa en. vald idag Dag %1$d av %2$d Visa skapandealternativ Skapa en ny kalenderhändelse - Skapa en ny kalendersamling + Skapa en ny kalender Ny kalender Ny kalenderhändelse Redigera kalender @@ -2702,7 +2702,7 @@ Lägg till i en av dina kalendrar Du har inte skapat några kalendrar än. Lägg till i telefonens kalender - Dela som nostr-länk + Dela som Nostr-länk Dela kalenderlänk Exportera till kalender (.ics) Dela kalenderhändelse @@ -2715,8 +2715,8 @@ ✓ Kommer Kanske \? Kanske - Kan inte - ✗ Kan inte + Kommer inte + ✗ Kommer inte Inga OSA än. calendar_reminders Kalenderpåminnelser @@ -2734,16 +2734,16 @@ Väntar på bekräftelse Nytt on-chain-belopp i sats Snabba on-chain-zapbelopp - On-chain-zappar betalar miner-avgifter, så beloppen är vanligtvis större än Lightning. Tryck på ett belopp för att ta bort det. + On-chain-zappar betalar miner-avgifter, så beloppen är vanligtvis större än för Lightning. Tryck på ett belopp för att ta bort det. Skicka on-chain istället Inga transaktioner matchar detta filter Kopiera adress Ingen on-chain-adress tillgänglig för detta konto. - Ingen chain-backend är konfigurerad. + Ingen on-chain-backend är konfigurerad. Väntar Offentlig Denna plånbok är offentlig - Din Taproot-adress härleds från din publika Nostr-nyckel, så alla som känner till din npub kan se denna plånboks saldo och transaktionshistorik på blockchain.\n\nFör att bevara din integritet, fyll på och töm denna plånbok från och till icke-privata konton, som börser. Blanda aldrig dessa medel med dina kalla plånböcker, och behandla dem som pengar du kan förlora, eftersom alla som har kontroll över din nsec kan spendera dem. + Din Taproot-adress härleds från din publika Nostr-nyckel, så alla som känner till din npub kan se denna plånboks saldo och transaktionshistorik på blockchainen.\n\nFör att bevara din integritet, fyll på denna plånbok från icke-privata konton (som börser) och skicka tillbaka medel dit. Blanda aldrig dessa medel med dina kalla plånböcker, och behandla dem som pengar du kan förlora, eftersom alla som har kontroll över din nsec kan spendera medlen. Uppfattat On-chain-transaktioner Nowhere Drop From 13c0e26de3456b104d0f3751baade15e4ed98694 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 19:28:30 +0000 Subject: [PATCH 10/21] refactor: filter followed chats out of the main list before rendering Avoids creating empty LazyColumn item slots for followed channels that were skipped at render time. --- .../loggedIn/publicChats/PublicChatsFeedLoaded.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt index ce61cb95ae..61d7133fd7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt @@ -62,6 +62,11 @@ fun PublicChatsFeedLoaded( } } + val unpinned = + remember(items.list, followedSet) { + items.list.filter { it.idHex !in followedSet } + } + LazyColumn( contentPadding = rememberFeedContentPadding(FeedPadding), state = listState, @@ -75,13 +80,11 @@ fun PublicChatsFeedLoaded( } itemsIndexed( - items.list, + unpinned, key = { _, item -> item.idHex }, contentType = { _, item -> item.event?.kind ?: -1 }, ) { _, item -> - if (item.idHex !in followedSet) { - PublicChatRow(item, accountViewModel, nav) - } + PublicChatRow(item, accountViewModel, nav) } } } From 9dcce46f67f365c99611fc14e8459c34c731015c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 19:43:08 +0000 Subject: [PATCH 11/21] feat(zaps): lower default zap amounts and add 1k-sat onchain floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Lightning quick-zap defaults: 100/500/1000 → 21/50/100 sats - Onchain quick-zap default: 10k → 5k sats - Block onchain zaps below 1000 sats in OnchainZapSendDialog so users aren't quietly creating transactions where miner fees dwarf the gift. The protocol dust threshold (330 sats) still applies underneath. --- .../model/AccountSyncedSettingsInternal.kt | 4 +-- .../amethyst/ui/note/UpdateZapAmountDialog.kt | 4 +-- .../amethyst/ui/note/ZapCustomDialog.kt | 2 +- .../loggedIn/wallet/OnchainZapSendDialog.kt | 25 ++++++++++++++++++- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt index 2e50447b7a..29ed96a2c0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSyncedSettingsInternal.kt @@ -37,8 +37,8 @@ val DefaultReactions = "\uD83D\uDE31", ) -val DefaultZapAmounts = listOf(100L, 500L, 1000L) -val DefaultOnchainZapAmounts = listOf(10_000L) +val DefaultZapAmounts = listOf(21L, 50L, 100L) +val DefaultOnchainZapAmounts = listOf(5_000L) val DefaultReportWarningThreshold = 5 @Serializable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt index 8ca2213069..ca6750051e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/UpdateZapAmountDialog.kt @@ -284,7 +284,7 @@ fun UpdateZapAmountContent( ), placeholder = { Text( - text = "100, 1000, 5000", + text = "21, 50, 100", color = MaterialTheme.colorScheme.placeholderText, ) }, @@ -406,7 +406,7 @@ fun UpdateZapAmountContent( ), placeholder = { Text( - text = "10000, 50000, 250000", + text = "5000, 25000, 100000", color = MaterialTheme.colorScheme.placeholderText, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt index 8c4e6feff4..5bd7d3624c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapCustomDialog.kt @@ -236,7 +236,7 @@ fun ZapCustomDialog( ), placeholder = { Text( - text = "100, 1000, 5000", + text = "21, 50, 100", color = MaterialTheme.colorScheme.placeholderText, ) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt index 716a79570c..000ca33990 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/OnchainZapSendDialog.kt @@ -99,6 +99,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.text.NumberFormat +// UX floor for onchain zaps. Below this any on-chain transaction is dominated +// by miner fees — the recipient nets close to nothing even at low fee rates, +// so quietly funneling the user to a Lightning zap is the friendlier outcome. +// This is stricter than the protocol-level [OnchainZapBuilder.DUST_THRESHOLD_SATS] +// (330 sats), which only guards against creating outputs the network rejects. +private const val MIN_ONCHAIN_ZAP_SATS = 1_000L + private enum class FeeTier( val label: String, val etaLabel: String, @@ -242,12 +249,14 @@ fun OnchainZapSendDialog( previewShares.orEmpty().filter { it.sats < OnchainZapBuilder.DUST_THRESHOLD_SATS } } + val belowMinimum = amountSats != null && amountSats > 0 && amountSats < MIN_ONCHAIN_ZAP_SATS + val canSend = !sending && result == null && (splitMode || (resolvedRecipient != null && resolvedRecipient != senderPubKey)) && amountSats != null && - amountSats > 0 && + amountSats >= MIN_ONCHAIN_ZAP_SATS && fees != null && (!splitMode || (previewShares != null && belowDustShares.isEmpty())) @@ -353,6 +362,7 @@ fun OnchainZapSendDialog( amountInput = amountInput, onAmountChange = { amountInput = it }, presetAmounts = presetAmounts, + belowMinimum = belowMinimum, ) SectionSpacer() @@ -610,6 +620,7 @@ private fun AmountSection( amountInput: String, onAmountChange: (String) -> Unit, presetAmounts: List, + belowMinimum: Boolean, ) { SectionLabel("Amount") @@ -636,8 +647,20 @@ private fun AmountSection( keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), placeholder = { Text("0") }, suffix = { Text("sats", color = MaterialTheme.colorScheme.onSurfaceVariant) }, + isError = belowMinimum, modifier = Modifier.fillMaxWidth(), ) + + if (belowMinimum) { + Spacer(Modifier.height(4.dp)) + Text( + text = + "Minimum on-chain zap is ${NumberFormat.getNumberInstance().format(MIN_ONCHAIN_ZAP_SATS)} sats — " + + "smaller amounts are eaten by miner fees. Use a Lightning zap instead.", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error, + ) + } } @OptIn(ExperimentalMaterial3Api::class) From a1cca4a748208977a8bba673cbd0206d6c77df24 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 20:05:02 +0000 Subject: [PATCH 12/21] fix(nwc): flush REQ before publishing event and surface silent errors Two problems caused wallet timeouts that affected only Amethyst users: 1. The NWC subscription went through a 500 ms BundledUpdate debounce while the request event was published immediately. Kind 23195 responses are ephemeral, so if the wallet replied faster than the REQ reached the relay, the reply was dropped with no replay. Add a synchronous subscribeAndFlush() that bypasses the bundler so the REQ is queued on the WebSocket before the EVENT. 2. Three failure paths were swallowed: decryption returning null, an unknown response subtype, and a response arriving with no matching pending request. Users saw "Wallet request timed out" with no clue. Surface a specific error in WalletViewModel for the first two, and log a warning in LocalCache for the third. --- .../amethyst/model/LocalCache.kt | 10 +++++- .../nip47WalletConnect/NwcSignerState.kt | 10 ++++-- .../nwc/NWCPaymentFilterAssembler.kt | 12 +++++++ .../screen/loggedIn/wallet/WalletViewModel.kt | 32 ++++++++++++++++--- 4 files changed, 57 insertions(+), 7 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 2d9671bf6d..12d34ba2d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2099,7 +2099,15 @@ object LocalCache : ILocalCache, ICacheProvider { wasVerified: Boolean, ): Boolean { val requestId = event.requestId() - val pending = paymentTracker.onResponseReceived(requestId) ?: return false + val pending = + paymentTracker.onResponseReceived(requestId) ?: run { + Log.w( + "LocalCache", + "NWC response ${event.id} from ${event.pubKey} references request e=$requestId but no pending request is registered. " + + "The response was either delivered after timeout, the user holds a stale subscription, or the wallet service set the wrong e tag.", + ) + return false + } val zappedNote = pending.zappedNote val responseCallback = pending.onResponse 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 5bf93e5ff2..1877c30d04 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 @@ -152,7 +152,11 @@ class NwcSignerState( val assembler = nwcFilterAssembler() - assembler.subscribe(filter) + // Synchronous flush so the REQ frame is queued on the WebSocket before + // the EVENT is published. Without this, the bundler may delay REQ up + // to 500ms, and the wallet service's ephemeral kind 23195 reply can + // be missed. + assembler.subscribeAndFlush(filter) scope.launch(Dispatchers.IO) { delay(60000) @@ -189,7 +193,9 @@ class NwcSignerState( val assembler = nwcFilterAssembler() - assembler.subscribe(filter) + // Synchronous flush so the REQ frame is queued before the EVENT. + // See sendNwcRequestToWallet above for the rationale. + assembler.subscribeAndFlush(filter) scope.launch(Dispatchers.IO) { delay(60000) // waits 1 minute to complete payment. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt index 48c66be4ee..46df66e2d3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/nwc/NWCPaymentFilterAssembler.kt @@ -48,5 +48,17 @@ class NWCPaymentFilterAssembler( override fun invalidateKeys() = invalidateFilters() + /** + * Synchronously sends the REQ frame to the relay, bypassing the 500ms + * BundledUpdate debounce. Used for NIP-47 RPC where the response is an + * ephemeral event (kind 23195) and the subscription must be active on the + * relay before we publish the request event — otherwise the relay drops + * the response with no replay. + */ + fun subscribeAndFlush(query: NWCPaymentQueryState) { + subscribe(query) + group.forEach { it.forceInvalidate() } + } + override fun destroy() = group.forEach { it.destroy() } } 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 f05e5c0a68..34f9bc673c 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 @@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcTransaction import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceErrorResponse import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayInvoiceSuccessResponse +import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -172,6 +173,19 @@ class WalletViewModel : ViewModel() { private val _receiveState = MutableStateFlow(ReceiveState.Idle) val receiveState = _receiveState.asStateFlow() + // A null `Response` means the wallet service replied but the payload could + // not be decrypted (wrong key, unexpected format). Any other unexpected + // subtype means the response shape didn't match any known NIP-47 result. + // Both used to be swallowed silently — now we surface them so the user + // can distinguish "wallet never answered" from "wallet answered with + // something we can't read". + private fun unreadableResponseError(response: Response?): String = + if (response == null) { + "Could not decrypt the wallet's reply — the wallet may be using a different key" + } else { + "Wallet returned an unrecognized reply for ${response.resultType}" + } + private fun launchTimeout(onTimeout: () -> Unit): Job = viewModelScope.launch(Dispatchers.IO) { delay(NWC_TIMEOUT_MS) @@ -315,7 +329,9 @@ class WalletViewModel : ViewModel() { } else -> { - updateWalletInfo(walletId) { it.copy(isLoading = false) } + updateWalletInfo(walletId) { + it.copy(error = unreadableResponseError(response), isLoading = false) + } } } } @@ -376,13 +392,16 @@ class WalletViewModel : ViewModel() { is GetBalanceSuccessResponse -> { _balanceSats.value = (response.result?.balance ?: 0L) / 1000L updateWalletInfo(walletId) { it.copy(balanceSats = _balanceSats.value) } + _error.value = null } is NwcErrorResponse -> { _error.value = response.error?.message ?: "Balance request failed" } - else -> {} + else -> { + _error.value = unreadableResponseError(response) + } } _isLoading.value = false } @@ -444,13 +463,16 @@ class WalletViewModel : ViewModel() { } else { txs.size >= pageSize } + _error.value = null } is NwcErrorResponse -> { _error.value = response.error?.message ?: "Failed to load transactions" } - else -> {} + else -> { + _error.value = unreadableResponseError(response) + } } _isLoading.value = false } @@ -498,7 +520,9 @@ class WalletViewModel : ViewModel() { _error.value = response.error?.message ?: "Failed to load more transactions" } - else -> {} + else -> { + _error.value = unreadableResponseError(response) + } } _isLoadingMore.value = false } From ba43edef57a3f3b783dd3758cadd1faac4b0ddf8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 20:45:31 +0000 Subject: [PATCH 13/21] feat: show pin icon on followed Public Chat cards Overlay a small push-pin glyph at the top-right of pinned channel cards so the user can tell at a glance why those rows are at the top. The underlying ChannelCardCompose is untouched; only the row wrapper changes to a Box to host the overlay. --- .../publicChats/PublicChatsFeedLoaded.kt | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt index 61d7133fd7..1b4c35c847 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt @@ -20,19 +20,26 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note @@ -42,6 +49,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding +import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent @Composable @@ -76,7 +84,7 @@ fun PublicChatsFeedLoaded( key = { item -> "pinned-" + item.idHex }, contentType = { item -> item.event?.kind ?: -1 }, ) { item -> - PublicChatRow(item, accountViewModel, nav) + PublicChatRow(item, pinned = true, accountViewModel, nav) } itemsIndexed( @@ -84,7 +92,7 @@ fun PublicChatsFeedLoaded( key = { _, item -> item.idHex }, contentType = { _, item -> item.event?.kind ?: -1 }, ) { _, item -> - PublicChatRow(item, accountViewModel, nav) + PublicChatRow(item, pinned = false, accountViewModel, nav) } } } @@ -92,10 +100,11 @@ fun PublicChatsFeedLoaded( @Composable private fun LazyItemScope.PublicChatRow( item: Note, + pinned: Boolean, accountViewModel: AccountViewModel, nav: INav, ) { - Row(Modifier.fillMaxWidth().animateItem()) { + Box(Modifier.fillMaxWidth().animateItem()) { ChannelCardCompose( baseNote = item, routeForLastRead = "PublicChatsFeed", @@ -104,6 +113,18 @@ private fun LazyItemScope.PublicChatRow( accountViewModel = accountViewModel, nav = nav, ) + if (pinned) { + Icon( + symbol = MaterialSymbols.PushPin, + contentDescription = null, + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(10.dp) + .size(16.dp), + tint = MaterialTheme.colorScheme.placeholderText, + ) + } } HorizontalDivider( From 5cdf69bb8392334fcb008814274e1597c766f1ee Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 20:52:55 +0000 Subject: [PATCH 14/21] fix: move pin badge to cover-image corner and add section gap - Pin glyph moved from top-right (overlapping LikeReaction/ZapReaction) to top-left, inside a surface-tinted circular badge that overlays the cover image corner. Reads against any cover image and frees the reaction buttons from being eclipsed and hit-tested through. - 8dp Spacer item inserted between the pinned items{} and unpinned itemsIndexed{} blocks, only when both sides have content, so the section boundary reads as a section break instead of just another row. --- .../publicChats/PublicChatsFeedLoaded.kt | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt index 1b4c35c847..8cc1a603c2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt @@ -20,8 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box 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 @@ -29,6 +31,7 @@ import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -49,7 +52,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent @Composable @@ -87,6 +89,12 @@ fun PublicChatsFeedLoaded( PublicChatRow(item, pinned = true, accountViewModel, nav) } + if (pinned.isNotEmpty() && unpinned.isNotEmpty()) { + item(key = "pinned-unpinned-gap", contentType = "section-gap") { + Box(Modifier.fillMaxWidth().height(8.dp)) + } + } + itemsIndexed( unpinned, key = { _, item -> item.idHex }, @@ -114,15 +122,11 @@ private fun LazyItemScope.PublicChatRow( nav = nav, ) if (pinned) { - Icon( - symbol = MaterialSymbols.PushPin, - contentDescription = null, + PinBadge( modifier = Modifier - .align(Alignment.TopEnd) - .padding(10.dp) - .size(16.dp), - tint = MaterialTheme.colorScheme.placeholderText, + .align(Alignment.TopStart) + .padding(start = 14.dp, top = 14.dp), ) } } @@ -131,3 +135,24 @@ private fun LazyItemScope.PublicChatRow( thickness = DividerThickness, ) } + +@Composable +private fun PinBadge(modifier: Modifier = Modifier) { + Box( + modifier = + modifier + .size(22.dp) + .background( + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.85f), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + symbol = MaterialSymbols.PushPin, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + } +} From ee0b658401d09e5b1314332f505012432e14abef Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 21:09:49 +0000 Subject: [PATCH 15/21] feat: keep Public Chats scrolled to top when new top items arrive When the first item of either the pinned or unpinned block changes, animate-scroll back to index 0 if the user was at or near the very top. This mirrors the pattern from ChatFeedView so a new chat bubbling up doesn't leave the user one row below it. --- .../screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt index 8cc1a603c2..b15269c53b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -77,6 +78,12 @@ fun PublicChatsFeedLoaded( items.list.filter { it.idHex !in followedSet } } + LaunchedEffect(pinned.firstOrNull()?.idHex, unpinned.firstOrNull()?.idHex) { + if (listState.firstVisibleItemIndex <= 1) { + listState.animateScrollToItem(0) + } + } + LazyColumn( contentPadding = rememberFeedContentPadding(FeedPadding), state = listState, From 3f91cb16890aaf0defc313155b83e242fa8b3229 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 21 May 2026 21:35:27 +0000 Subject: [PATCH 16/21] fix: actually fetch ChannelCreateEvent (kind 40) and widen the relay set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit filterMissingChannelsById had an inverted isEmpty() check that emitted zero filters, so kind 40 was never requested from any relay. Channels discovered from kind 42 messages stayed as empty stubs unless the creator also happened to publish a kind 41 metadata update findable on the same relay — which is why most cards in the Public Chats feed loaded with no name or picture. Fix: - Drop the inverted condition; mapOfSet guarantees non-empty values, so emit a RelayBasedFilter for every (relay, channelIds) entry. - Widen the relay set per channel to include the user's search and indexer relay lists. Falls back to DefaultSearchRelayList / DefaultIndexerRelayList when those lists are empty. - Plumb the Account through ChannelFinderQueryState so the assembler can read the search/indexer flows. Mirrors EventFinderQueryState. --- .../ChannelFinderFilterAssemblyGroup.kt | 2 + ...ChannelFinderFilterAssemblySubscription.kt | 8 ++-- .../FilterChannelMetadataCreationById.kt | 37 ++++++++++--------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt index 9ea5235108..3f12d0bd05 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive.ChannelMetadataAndLiveActivityWatcherSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.ChannelLoaderSubAssembler import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient @@ -31,6 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient @Stable class ChannelFinderQueryState( val channel: Channel, + val account: Account, ) @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt index 8d5f6828ac..3bc756a125 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt @@ -24,24 +24,26 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @Composable fun ChannelFinderFilterAssemblerSubscription( channel: Channel, accountViewModel: AccountViewModel, -) = ChannelFinderFilterAssemblerSubscription(channel, accountViewModel.dataSources().channelFinder) +) = ChannelFinderFilterAssemblerSubscription(channel, accountViewModel.account, accountViewModel.dataSources().channelFinder) @Composable fun ChannelFinderFilterAssemblerSubscription( channel: Channel, + account: Account, dataSource: ChannelFinderFilterAssemblyGroup, ) { // different screens get different states // even if they are tracking the same tag. val state = - remember(channel) { - ChannelFinderQueryState(channel) + remember(channel, account) { + ChannelFinderQueryState(channel, account) } LifecycleAwareKeyDataSourceSubscription(state, dataSource) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt index f8c58f0910..c10fe0477e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats +import com.vitorpamplona.amethyst.commons.defaults.DefaultIndexerRelayList +import com.vitorpamplona.amethyst.commons.defaults.DefaultSearchRelayList import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -34,29 +36,28 @@ fun filterMissingChannelsById(keys: List): List if (key.channel is PublicChatChannel && key.channel.event == null) { - key.channel.relays().forEach { + val searchRelays = + key.account.searchRelayList.flow.value + .ifEmpty { DefaultSearchRelayList } + val indexerRelays = + key.account.indexerRelayList.flow.value + .ifEmpty { DefaultIndexerRelayList } + + (key.channel.relays() + searchRelays + indexerRelays).forEach { add(it, key.channel.idHex) } - } else { - null } } } - if (relayPerChannel.isEmpty()) return emptyList() - - return relayPerChannel.mapNotNull { - if (it.value.isEmpty()) { - RelayBasedFilter( - relay = it.key, - filter = - Filter( - kinds = filterMissingPublicChannelsByIdKinds, - ids = it.value.sorted(), - ), - ) - } else { - null - } + return relayPerChannel.map { (relay, channelIds) -> + RelayBasedFilter( + relay = relay, + filter = + Filter( + kinds = filterMissingPublicChannelsByIdKinds, + ids = channelIds.sorted(), + ), + ) } } From 27543ac304662c65a969835a390aedf232f020f0 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 22 May 2026 07:00:31 +0300 Subject: [PATCH 17/21] feat(desktop): long-press details popups + right-click customize for note actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Long-press zap icon → floating popup with zap receipts (sender, amount, message) - Long-press like icon → floating popup with reactions grouped by emoji - Right-click like icon → emoji picker (DropdownMenu with 6 common emojis) - Right-click repost icon → Repost/Quote options (DropdownMenu) - Right-click zap icon → custom zap dialog (preserved existing behavior) - Long-press reply → opens thread (same as click) - ActivePopup sealed class ensures only one popup open at a time - Popup + ElevatedCard for rich content, DropdownMenu for option lists - combinedClickable with explicit ripple preserves IconButton UX - PopupProperties(focusable = true) for desktop click-outside dismiss - @Immutable on ZapReceipt for Compose stability - Note param added to NoteActionsRow, passed from FeedScreen Co-Authored-By: Claude Opus 4.6 (1M context) --- .../amethyst/desktop/ui/FeedScreen.kt | 2 + .../amethyst/desktop/ui/NoteActions.kt | 562 +++++++++++++++--- 2 files changed, 479 insertions(+), 85 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index b8acdd51a3..9eecca9937 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -209,6 +209,7 @@ fun FeedNoteCard( onReplyClick = onReply, onZapFeedback = onZapFeedback, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + note = originalNote, zapCount = originalNote.zaps.size, zapAmountSats = zapAmount.toLong(), zapReceipts = emptyList(), @@ -257,6 +258,7 @@ fun FeedNoteCard( onReplyClick = onReply, onZapFeedback = onZapFeedback, modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp), + note = note, zapCount = note.zaps.size, zapAmountSats = zapAmount.toLong(), zapReceipts = emptyList(), diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index 490125e088..4be1dbb783 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -20,7 +20,10 @@ */ package com.vitorpamplona.amethyst.desktop.ui +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -28,19 +31,28 @@ 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.heightIn +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedCard import androidx.compose.material3.FilterChip +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -49,8 +61,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.isSecondaryPressed +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties import com.vitorpamplona.amethyst.commons.icons.Bookmark import com.vitorpamplona.amethyst.commons.icons.BookmarkFilled import com.vitorpamplona.amethyst.commons.icons.Reply @@ -58,6 +79,7 @@ import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.icons.Zap import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.commons.model.nip51Bookmarks.BookmarkAction @@ -89,6 +111,22 @@ import kotlin.coroutines.resume private val ZAP_AMOUNTS = listOf(21L, 100L, 500L, 1000L, 5000L, 10000L) +/** + * Mutually exclusive popup state for note action bar. + * Only one popup can be open at a time. + */ +sealed class ActivePopup { + data object None : ActivePopup() + + data object ZapReceipts : ActivePopup() + + data object Reactions : ActivePopup() + + data object EmojiPicker : ActivePopup() + + data object RepostOptions : ActivePopup() +} + /** * Feedback from a zap operation for UI display. */ @@ -115,6 +153,7 @@ sealed class ZapFeedback { /** * Data class representing a zap receipt for display. */ +@Immutable data class ZapReceipt( val senderPubKey: String, val amountSats: Long, @@ -395,6 +434,214 @@ fun ZapReceiptsDialog( ) } +/** + * Floating popup showing zap receipts from a Note's zaps map. + * Uses Popup + ElevatedCard for rich scrollable content. + */ +@Composable +fun ZapReceiptsPopup( + note: Note, + localCache: DesktopLocalCache, + onDismiss: () -> Unit, +) { + val zapEntries = + remember(note.zaps) { + note.zaps + .mapNotNull { (request, receipt) -> + val sender = + request.author?.toBestDisplayName() + ?: request.event?.pubKey?.take(12) + ?: return@mapNotNull null + val amount = + (receipt?.event as? LnZapEvent)?.amount?.toLong() + ?: return@mapNotNull null + val message = request.event?.content?.ifBlank { null } + Triple(sender, amount, message) + }.sortedByDescending { it.second } + } + + val totalSats = remember(zapEntries) { zapEntries.sumOf { it.second } } + + Popup( + alignment = Alignment.TopCenter, + offset = IntOffset(0, -40), + onDismissRequest = onDismiss, + properties = PopupProperties(focusable = true), + ) { + ElevatedCard( + modifier = Modifier.widthIn(max = 280.dp), + ) { + Column( + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .heightIn(max = 300.dp) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (zapEntries.isEmpty()) { + Text( + "No zaps yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + // Header: total sats + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + Zap, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp), + ) + Text( + "${formatSats(totalSats)} sats", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary, + ) + } + + HorizontalDivider() + + // Sorted receipts + zapEntries.take(10).forEach { (sender, amount, message) -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = sender, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + if (!message.isNullOrBlank()) { + Text( + text = message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + ) + } + } + Text( + text = "${formatSats(amount)} sats", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + ) + } + } + if (zapEntries.size > 10) { + Text( + text = "and ${zapEntries.size - 10} more...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + +/** + * Floating popup showing reactions grouped by emoji from a Note's reactions map. + * Uses Popup + ElevatedCard for rich scrollable content. + */ +@Composable +fun ReactionsPopup( + note: Note, + localCache: DesktopLocalCache, + onDismiss: () -> Unit, +) { + val totalCount = remember(note.reactions) { note.countReactions() } + + Popup( + alignment = Alignment.TopCenter, + offset = IntOffset(0, -40), + onDismissRequest = onDismiss, + properties = PopupProperties(focusable = true), + ) { + ElevatedCard( + modifier = Modifier.widthIn(max = 280.dp), + ) { + Column( + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .heightIn(max = 300.dp) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (note.reactions.isEmpty()) { + Text( + "No reactions yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + // Header: total count + Text( + "$totalCount reaction${if (totalCount != 1) "s" else ""}", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + + HorizontalDivider() + + // Group by emoji + note.reactions.forEach { (emoji, reactionNotes) -> + val displayEmoji = if (emoji == "+") "\u2764\ufe0f" else emoji + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + displayEmoji, + fontSize = 16.sp, + ) + Text( + "${reactionNotes.size}", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + // Sender names + reactionNotes.take(5).forEach { reactionNote -> + val senderName = + reactionNote.author?.toBestDisplayName() + ?: reactionNote.event?.pubKey?.take(12) + ?: "Unknown" + Text( + text = senderName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 24.dp), + ) + } + if (reactionNotes.size > 5) { + Text( + text = "and ${reactionNotes.size - 5} more...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(start = 24.dp), + ) + } + } + } + } + } + } + } +} + /** * Fetches metadata for multiple users in a single subscription. */ @@ -470,9 +717,13 @@ private suspend fun fetchMetadataForUsers( } } +private val EMOJI_OPTIONS = listOf("+", "\u2764\ufe0f", "\ud83e\udd19", "\ud83d\udd25", "\ud83d\udc40", "\ud83d\ude02") + /** * Action buttons row for a note (react, reply, repost, zap, bookmark). + * Supports click (action), long-press (view details popup), and right-click (customize). */ +@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @Composable fun NoteActionsRow( event: Event, @@ -482,6 +733,7 @@ fun NoteActionsRow( onReplyClick: () -> Unit, onZapFeedback: (ZapFeedback) -> Unit, modifier: Modifier = Modifier, + note: Note? = null, zapCount: Int = 0, zapAmountSats: Long = 0, zapReceipts: List = emptyList(), @@ -502,16 +754,27 @@ fun NoteActionsRow( var showZapReceiptsDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() + // Mutually exclusive popup state + var activePopup by remember { mutableStateOf(ActivePopup.None) } + Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically, ) { - // Reply button with count + // Reply button with count — long-press = same as click (open thread) Row(verticalAlignment = Alignment.CenterVertically) { - IconButton( - onClick = onReplyClick, - modifier = Modifier.size(32.dp), + Box( + modifier = + Modifier + .size(32.dp) + .combinedClickable( + onClick = onReplyClick, + onLongClick = onReplyClick, + indication = ripple(bounded = false, radius = 16.dp), + interactionSource = remember { MutableInteractionSource() }, + ), + contentAlignment = Alignment.Center, ) { Icon( Reply, @@ -529,37 +792,91 @@ fun NoteActionsRow( } } - // Like button with count + // Like button with count — long-press = reactions popup, right-click = emoji picker Row(verticalAlignment = Alignment.CenterVertically) { - IconButton( - onClick = { - if (!isLiked) { - scope.launch { - reactToNote( - // TODO: Bring a hint to where the event came from - event = EventHintBundle(event, null), - reaction = "+", - account = account, - relayManager = relayManager, - ) - isLiked = true - localReactionCount++ - } + Box { + Box( + modifier = + Modifier + .size(32.dp) + .combinedClickable( + onClick = { + if (!isLiked) { + scope.launch { + reactToNote( + event = EventHintBundle(event, null), + reaction = "+", + account = account, + relayManager = relayManager, + ) + isLiked = true + localReactionCount++ + } + } + }, + onLongClick = { + if (note != null) { + activePopup = ActivePopup.Reactions + } + }, + indication = ripple(bounded = false, radius = 16.dp), + interactionSource = remember { MutableInteractionSource() }, + ).onPointerEvent(PointerEventType.Press) { pointerEvent -> + if (pointerEvent.buttons.isSecondaryPressed) { + activePopup = ActivePopup.EmojiPicker + } + }, + contentAlignment = Alignment.Center, + ) { + Icon( + if (isLiked) MaterialSymbols.Favorite else MaterialSymbols.FavoriteBorder, + contentDescription = if (isLiked) "Unlike" else "Like", + tint = + if (isLiked) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(18.dp), + ) + } + + // Reactions popup (long-press) + if (activePopup is ActivePopup.Reactions && note != null) { + ReactionsPopup( + note = note, + localCache = localCache, + onDismiss = { activePopup = ActivePopup.None }, + ) + } + + // Emoji picker (right-click) + DropdownMenu( + expanded = activePopup is ActivePopup.EmojiPicker, + onDismissRequest = { activePopup = ActivePopup.None }, + ) { + EMOJI_OPTIONS.forEach { emoji -> + val displayEmoji = if (emoji == "+") "\u2764\ufe0f" else emoji + DropdownMenuItem( + text = { Text(displayEmoji, fontSize = 20.sp) }, + onClick = { + activePopup = ActivePopup.None + if (!isLiked) { + scope.launch { + reactToNote( + event = EventHintBundle(event, null), + reaction = emoji, + account = account, + relayManager = relayManager, + ) + isLiked = true + localReactionCount++ + } + } + }, + ) } - }, - modifier = Modifier.size(32.dp), - ) { - Icon( - if (isLiked) MaterialSymbols.Favorite else MaterialSymbols.FavoriteBorder, - contentDescription = if (isLiked) "Unlike" else "Like", - tint = - if (isLiked) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - modifier = Modifier.size(18.dp), - ) + } } if (localReactionCount > 0) { Text( @@ -570,36 +887,82 @@ fun NoteActionsRow( } } - // Repost button with count + // Repost button with count — right-click = repost options Row(verticalAlignment = Alignment.CenterVertically) { - IconButton( - onClick = { - if (!isReposted) { - scope.launch { - repostNote( - // TODO: Bring a hint to where the event came from - event = EventHintBundle(event, null), - account = account, - relayManager = relayManager, - ) - isReposted = true - localRepostCount++ - } - } - }, - modifier = Modifier.size(32.dp), - ) { - Icon( - Repost, - contentDescription = "Repost", - tint = - if (isReposted) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant + Box { + Box( + modifier = + Modifier + .size(32.dp) + .combinedClickable( + onClick = { + if (!isReposted) { + scope.launch { + repostNote( + event = EventHintBundle(event, null), + account = account, + relayManager = relayManager, + ) + isReposted = true + localRepostCount++ + } + } + }, + onLongClick = { /* no long-press action for repost */ }, + indication = ripple(bounded = false, radius = 16.dp), + interactionSource = remember { MutableInteractionSource() }, + ).onPointerEvent(PointerEventType.Press) { pointerEvent -> + if (pointerEvent.buttons.isSecondaryPressed) { + activePopup = ActivePopup.RepostOptions + } + }, + contentAlignment = Alignment.Center, + ) { + Icon( + Repost, + contentDescription = "Repost", + tint = + if (isReposted) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(18.dp), + ) + } + + // Repost options (right-click) + DropdownMenu( + expanded = activePopup is ActivePopup.RepostOptions, + onDismissRequest = { activePopup = ActivePopup.None }, + ) { + DropdownMenuItem( + text = { Text("Repost") }, + onClick = { + activePopup = ActivePopup.None + if (!isReposted) { + scope.launch { + repostNote( + event = EventHintBundle(event, null), + account = account, + relayManager = relayManager, + ) + isReposted = true + localRepostCount++ + } + } }, - modifier = Modifier.size(18.dp), - ) + ) + DropdownMenuItem( + text = { Text("Quote") }, + onClick = { + activePopup = ActivePopup.None + // Copy note link to clipboard for quoting + val noteLink = "nostr:${NNote.create(event.id)}" + copyToClipboard(noteLink) + }, + ) + } } if (localRepostCount > 0) { Text( @@ -610,33 +973,62 @@ fun NoteActionsRow( } } - // Zap button with amount (clickable to show receipts) + // Zap button with amount — long-press = zap receipts popup, right-click = custom zap dialog Row(verticalAlignment = Alignment.CenterVertically) { - Box(modifier = Modifier.size(32.dp), contentAlignment = Alignment.Center) { - if (isZapping) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.primary, - ) - } else { - IconButton( - onClick = { showZapDialog = true }, - modifier = Modifier.size(32.dp), - ) { - Icon( - Zap, - contentDescription = "Zap", - tint = - if (zapAmountSats > 0) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurfaceVariant - }, - modifier = Modifier.size(18.dp), + Box { + Box(modifier = Modifier.size(32.dp), contentAlignment = Alignment.Center) { + if (isZapping) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary, ) + } else { + Box( + modifier = + Modifier + .size(32.dp) + .combinedClickable( + onClick = { showZapDialog = true }, + onLongClick = { + if (note != null) { + activePopup = ActivePopup.ZapReceipts + } else { + showZapReceiptsDialog = true + } + }, + indication = ripple(bounded = false, radius = 16.dp), + interactionSource = remember { MutableInteractionSource() }, + ).onPointerEvent(PointerEventType.Press) { pointerEvent -> + if (pointerEvent.buttons.isSecondaryPressed) { + showZapDialog = true + } + }, + contentAlignment = Alignment.Center, + ) { + Icon( + Zap, + contentDescription = "Zap", + tint = + if (zapAmountSats > 0) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(18.dp), + ) + } } } + + // Zap receipts popup (long-press) + if (activePopup is ActivePopup.ZapReceipts && note != null) { + ZapReceiptsPopup( + note = note, + localCache = localCache, + onDismiss = { activePopup = ActivePopup.None }, + ) + } } if (zapAmountSats > 0) { Text( @@ -792,7 +1184,7 @@ fun NoteActionsRow( ) } - // Zap receipts dialog + // Zap receipts dialog (from clicking the amount text) if (showZapReceiptsDialog) { ZapReceiptsDialog( receipts = zapReceipts, From 3185df21b6e0e7a79f3ca622a650615067cb1131 Mon Sep 17 00:00:00 2001 From: nrobi144 Date: Fri, 22 May 2026 07:37:53 +0300 Subject: [PATCH 18/21] fix(desktop): reactive counters, quote boost, and boost detail popup - Add kind 1 (replies) to interaction subscriptions - Key count reads on FlowSet state for reactive updates - Wire Quote menu item to ComposeNoteDialog with q-tag support - Add BoostsPopup on long-press repost icon (who boosted) - ComposeNoteDialog now accepts quoteOf param with nostr: URI pre-fill Co-Authored-By: Claude Opus 4.6 (1M context) --- .../DesktopRelaySubscriptionsCoordinator.kt | 5 + .../amethyst/desktop/ui/ComposeNoteDialog.kt | 37 ++- .../amethyst/desktop/ui/FeedScreen.kt | 77 +++-- .../amethyst/desktop/ui/NoteActions.kt | 271 ++++++++++++++++-- .../amethyst/desktop/ui/ThreadScreen.kt | 39 +++ 5 files changed, 371 insertions(+), 58 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt index 0a195b25bf..afa616fb00 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/DesktopRelaySubscriptionsCoordinator.kt @@ -175,6 +175,11 @@ class DesktopRelaySubscriptionsCoordinator( kinds = listOf(com.vitorpamplona.quartz.nip18Reposts.RepostEvent.KIND), tags = mapOf("e" to noteIds), ), + // Replies (kind 1) targeting these notes + Filter( + kinds = listOf(com.vitorpamplona.quartz.nip10Notes.TextNoteEvent.KIND), + tags = mapOf("e" to noteIds), + ), ) val listener = diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index 50b1715581..ab5a6c3ef7 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -74,6 +74,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.content.findHashtags import com.vitorpamplona.quartz.nip10Notes.content.findURLs +import com.vitorpamplona.quartz.nip18Reposts.quotes.QEventTag +import com.vitorpamplona.quartz.nip18Reposts.quotes.quote +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip92IMeta.IMetaTag import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -96,8 +99,19 @@ fun ComposeNoteDialog( relayManager: DesktopRelayConnectionManager, account: AccountState.LoggedIn, replyTo: Event? = null, + quoteOf: Event? = null, ) { - var content by remember { mutableStateOf("") } + val initialContent = + remember(quoteOf) { + if (quoteOf != null) { + val relays = relayManager.connectedRelays.value.take(3) + val nevent = NEvent.create(quoteOf.id, quoteOf.pubKey, quoteOf.kind, relays) + "\nnostr:$nevent" + } else { + "" + } + } + var content by remember { mutableStateOf(initialContent) } var isPosting by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() @@ -165,7 +179,11 @@ fun ComposeNoteDialog( ) { Column(modifier = Modifier.padding(24.dp)) { Text( - if (replyTo != null) "Reply" else "New Note", + when { + replyTo != null -> "Reply" + quoteOf != null -> "Quote" + else -> "New Note" + }, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onSurface, ) @@ -179,6 +197,15 @@ fun ComposeNoteDialog( ) } + quoteOf?.let { quoted -> + Spacer(Modifier.height(8.dp)) + Text( + "Quoting: ${quoted.content.take(50)}${if (quoted.content.length > 50) "..." else ""}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + Spacer(Modifier.height(16.dp)) OutlinedTextField( @@ -357,6 +384,7 @@ fun ComposeNoteDialog( account = account, relayManager = relayManager, replyTo = replyTo, + quoteOf = quoteOf, imetaTags = imetaTags, relays = selectedRelays, ) @@ -529,6 +557,7 @@ private suspend fun publishNote( account: AccountState.LoggedIn, relayManager: DesktopRelayConnectionManager, replyTo: Event?, + quoteOf: Event? = null, imetaTags: List = emptyList(), relays: Set, ) { @@ -546,6 +575,10 @@ private suspend fun publishNote( eTag(etag) pTag(PTag(replyTo.pubKey, relayHint = null)) } + if (quoteOf != null) { + quote(QEventTag(quoteOf.id, relayHint = null, authorPubKeyHex = quoteOf.pubKey)) + pTag(PTag(quoteOf.pubKey, relayHint = null)) + } hashtags(findHashtags(content)) references(findURLs(content)) for (imeta in imetaTags) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index 9eecca9937..8c666e145c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -87,6 +87,7 @@ import com.vitorpamplona.amethyst.desktop.ui.relay.LocalRelayCategories import com.vitorpamplona.amethyst.desktop.ui.relay.Nip65RelayEditor import com.vitorpamplona.amethyst.desktop.viewmodels.DesktopFeedViewModel import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser @@ -153,10 +154,10 @@ fun FeedNoteCard( return } - val reactionCount = originalNote.countReactions() - val replyCount = originalNote.replies.size - val repostCount = originalNote.boosts.size - val zapAmount = originalNote.zapsAmount + val reactionCount = remember(reactionsState) { originalNote.countReactions() } + val replyCount = remember(repliesState) { originalNote.replies.size } + val repostCount = remember(metadataState) { originalNote.boosts.size } + val zapAmount = remember(zapsState) { originalNote.zapsAmount } val reposterUser = localCache.getUserIfExists(event.pubKey) val originalUser = localCache.getUserIfExists(originalEvent.pubKey) @@ -170,15 +171,15 @@ fun FeedNoteCard( GenericRepostLayout( baseAuthorPicture = { UserAvatar( - userHex = originalEvent.pubKey, - pictureUrl = originalUser?.profilePicture(), + userHex = event.pubKey, + pictureUrl = reposterUser?.profilePicture(), size = 35.dp, ) }, repostAuthorPicture = { UserAvatar( - userHex = event.pubKey, - pictureUrl = reposterUser?.profilePicture(), + userHex = originalEvent.pubKey, + pictureUrl = originalUser?.profilePicture(), size = 35.dp, ) }, @@ -216,6 +217,8 @@ fun FeedNoteCard( reactionCount = reactionCount, replyCount = replyCount, repostCount = repostCount, + onNavigateToThread = onNavigateToThread, + onNavigateToProfile = onNavigateToProfile, ) } } @@ -227,10 +230,10 @@ fun FeedNoteCard( val repliesState by flowSet.replies.stateFlow.collectAsState() val zapsState by flowSet.zaps.stateFlow.collectAsState() - val reactionCount = note.countReactions() - val replyCount = note.replies.size - val repostCount = note.boosts.size - val zapAmount = note.zapsAmount + val reactionCount = remember(reactionsState) { note.countReactions() } + val replyCount = remember(repliesState) { note.replies.size } + val repostCount = remember(metadataState) { note.boosts.size } + val zapAmount = remember(zapsState) { note.zapsAmount } DisposableEffect(note) { onDispose { note.clearFlow() } @@ -265,6 +268,8 @@ fun FeedNoteCard( reactionCount = reactionCount, replyCount = replyCount, repostCount = repostCount, + onNavigateToThread = onNavigateToThread, + onNavigateToProfile = onNavigateToProfile, ) } } @@ -409,6 +414,15 @@ fun FeedScreen( val feedState by viewModel.feedState.feedContent.collectAsState() + // Force refresh when followedUsers arrives and feed is still empty + LaunchedEffect(followedUsers, feedState) { + if (followedUsers.isNotEmpty() && feedState is FeedState.Empty) { + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + viewModel.feedState.refreshSuspended() + } + } + } + // Viewport-aware metadata loading: only fetch for visible notes + buffer // Uses snapshotFlow to avoid per-frame recomposition from scroll observation LaunchedEffect(feedState, subscriptionsCoordinator) { @@ -511,19 +525,32 @@ fun FeedScreen( ) } - // Request interaction subscriptions — keyed on feedMode (stable), not feedState (changes every 250ms) - DisposableEffect(feedMode, subscriptionsCoordinator) { - val coordinator = subscriptionsCoordinator ?: return@DisposableEffect onDispose {} - val relays = relayManager.relayStatuses.value.keys - // Initial subscription with whatever notes are visible now - val noteIds = viewModel.feedState.visibleNotes().mapNotNull { it.event?.id } - val subId = - if (noteIds.isNotEmpty()) { - coordinator.requestInteractions(noteIds, relays) - } else { - null - } - onDispose { subId?.let { coordinator.releaseInteractions(it) } } + // Interaction subscriptions (reactions, zaps, reposts, replies) — same pattern as metadata + val interactionNoteIds = + remember(feedState) { + if (feedState !is FeedState.Loaded) return@remember emptyList() + viewModel.feedState.visibleNotes().mapNotNull { it.event?.id } + } + + rememberSubscription(allRelayUrls, interactionNoteIds, relayManager = relayManager) { + if (allRelayUrls.isEmpty() || interactionNoteIds.isEmpty()) return@rememberSubscription null + SubscriptionConfig( + subId = generateSubId("fetch-interactions"), + filters = + listOf( + FilterBuilders.reactionsForEvents(interactionNoteIds), + FilterBuilders.zapsForEvents(interactionNoteIds), + FilterBuilders.repostsForEvents(interactionNoteIds), + Filter( + kinds = listOf(com.vitorpamplona.quartz.nip10Notes.TextNoteEvent.KIND), + tags = mapOf("e" to interactionNoteIds), + ), + ), + relays = allRelayUrls, + onEvent = { event, _, relay, _ -> + subscriptionsCoordinator?.consumeEvent(event, relay) + }, + ) } Box(modifier = Modifier.fillMaxSize()) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index 4be1dbb783..8f90bfdf00 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.commons.model.nip51Bookmarks.BookmarkAction import com.vitorpamplona.amethyst.commons.model.nip57Zaps.ZapAction import com.vitorpamplona.amethyst.commons.services.lnurl.LightningAddressResolver +import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.network.DesktopHttpClient @@ -125,6 +126,8 @@ sealed class ActivePopup { data object EmojiPicker : ActivePopup() data object RepostOptions : ActivePopup() + + data object Boosts : ActivePopup() } /** @@ -442,25 +445,52 @@ fun ZapReceiptsDialog( fun ZapReceiptsPopup( note: Note, localCache: DesktopLocalCache, + relayManager: DesktopRelayConnectionManager, onDismiss: () -> Unit, + onNavigateToProfile: (String) -> Unit = {}, ) { + var metadataVersion by remember { mutableIntStateOf(0) } + + // Fetch missing metadata for zap senders + LaunchedEffect(note.idHex) { + val pubKeys = + note.zaps.keys + .mapNotNull { it.event?.pubKey } + .distinct() + .filter { localCache.getUserIfExists(it)?.profilePicture() == null } + if (pubKeys.isNotEmpty()) { + fetchMetadataForUsers(pubKeys, relayManager, localCache) { metadataVersion++ } + } + } + + @Suppress("UNUSED_EXPRESSION") + metadataVersion + + data class ZapEntry( + val pubKey: String, + val pictureUrl: String?, + val name: String, + val amount: Long, + val message: String?, + ) + val zapEntries = - remember(note.zaps) { + remember(note.zaps, metadataVersion) { note.zaps .mapNotNull { (request, receipt) -> - val sender = - request.author?.toBestDisplayName() - ?: request.event?.pubKey?.take(12) - ?: return@mapNotNull null + val pubKey = request.event?.pubKey ?: return@mapNotNull null + val user = request.author + val name = user?.toBestDisplayName() ?: pubKey.take(12) + val pictureUrl = user?.profilePicture() val amount = (receipt?.event as? LnZapEvent)?.amount?.toLong() ?: return@mapNotNull null val message = request.event?.content?.ifBlank { null } - Triple(sender, amount, message) - }.sortedByDescending { it.second } + ZapEntry(pubKey, pictureUrl, name, amount, message) + }.sortedByDescending { it.amount } } - val totalSats = remember(zapEntries) { zapEntries.sumOf { it.second } } + val totalSats = remember(zapEntries) { zapEntries.sumOf { it.amount } } Popup( alignment = Alignment.TopCenter, @@ -508,21 +538,30 @@ fun ZapReceiptsPopup( HorizontalDivider() // Sorted receipts - zapEntries.take(10).forEach { (sender, amount, message) -> + zapEntries.take(10).forEach { entry -> Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, + modifier = + Modifier.fillMaxWidth().clickable { + onDismiss() + onNavigateToProfile(entry.pubKey) + }, + horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { + UserAvatar( + userHex = entry.pubKey, + pictureUrl = entry.pictureUrl, + size = 24.dp, + ) Column(modifier = Modifier.weight(1f)) { Text( - text = sender, + text = entry.name, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface, ) - if (!message.isNullOrBlank()) { + if (!entry.message.isNullOrBlank()) { Text( - text = message, + text = entry.message, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, @@ -530,7 +569,7 @@ fun ZapReceiptsPopup( } } Text( - text = "${formatSats(amount)} sats", + text = "${formatSats(entry.amount)} sats", style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.primary, ) @@ -557,9 +596,28 @@ fun ZapReceiptsPopup( fun ReactionsPopup( note: Note, localCache: DesktopLocalCache, + relayManager: DesktopRelayConnectionManager, onDismiss: () -> Unit, + onNavigateToProfile: (String) -> Unit = {}, ) { - val totalCount = remember(note.reactions) { note.countReactions() } + var metadataVersion by remember { mutableIntStateOf(0) } + + LaunchedEffect(note.idHex) { + val pubKeys = + note.reactions.values + .flatten() + .mapNotNull { it.event?.pubKey } + .distinct() + .filter { localCache.getUserIfExists(it)?.profilePicture() == null } + if (pubKeys.isNotEmpty()) { + fetchMetadataForUsers(pubKeys, relayManager, localCache) { metadataVersion++ } + } + } + + @Suppress("UNUSED_EXPRESSION") + metadataVersion + + val totalCount = remember(note.reactions, metadataVersion) { note.countReactions() } Popup( alignment = Alignment.TopCenter, @@ -613,18 +671,31 @@ fun ReactionsPopup( color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - // Sender names + // Sender avatars + names reactionNotes.take(5).forEach { reactionNote -> - val senderName = - reactionNote.author?.toBestDisplayName() - ?: reactionNote.event?.pubKey?.take(12) - ?: "Unknown" - Text( - text = senderName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 24.dp), - ) + val pubKey = reactionNote.event?.pubKey ?: return@forEach + val user = reactionNote.author + val senderName = user?.toBestDisplayName() ?: pubKey.take(12) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = + Modifier.padding(start = 24.dp).clickable { + onDismiss() + onNavigateToProfile(pubKey) + }, + ) { + UserAvatar( + userHex = pubKey, + pictureUrl = user?.profilePicture(), + size = 20.dp, + ) + Text( + text = senderName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } if (reactionNotes.size > 5) { Text( @@ -642,6 +713,111 @@ fun ReactionsPopup( } } +/** + * Floating popup showing who boosted (reposted) a note. + * Shows kind 6/1621 reposts only (matching Android — quotes are not aggregated on Note). + * Uses Popup + ElevatedCard for rich scrollable content. + */ +@Composable +fun BoostsPopup( + note: Note, + localCache: DesktopLocalCache, + relayManager: DesktopRelayConnectionManager, + onDismiss: () -> Unit, + onNavigateToThread: (String) -> Unit = {}, + onNavigateToProfile: (String) -> Unit = {}, +) { + var metadataVersion by remember { mutableIntStateOf(0) } + + data class BoostEntry( + val pubKey: String, + val pictureUrl: String?, + val name: String, + ) + + LaunchedEffect(note.idHex) { + val pubKeys = + note.boosts + .mapNotNull { it.event?.pubKey } + .distinct() + .filter { localCache.getUserIfExists(it)?.profilePicture() == null } + if (pubKeys.isNotEmpty()) { + fetchMetadataForUsers(pubKeys, relayManager, localCache) { metadataVersion++ } + } + } + + @Suppress("UNUSED_EXPRESSION") + metadataVersion + + val boostEntries = + remember(note.boosts, metadataVersion) { + note.boosts.mapNotNull { boostNote -> + val pubKey = boostNote.event?.pubKey ?: return@mapNotNull null + val user = boostNote.author + BoostEntry(pubKey, user?.profilePicture(), user?.toBestDisplayName() ?: pubKey.take(12)) + } + } + + Popup( + alignment = Alignment.TopCenter, + offset = IntOffset(0, -40), + onDismissRequest = onDismiss, + properties = PopupProperties(focusable = true), + ) { + ElevatedCard( + modifier = Modifier.widthIn(max = 280.dp), + ) { + Column( + modifier = + Modifier + .verticalScroll(rememberScrollState()) + .heightIn(max = 300.dp) + .padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (boostEntries.isEmpty()) { + Text( + "No reposts yet", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } else { + Text( + "${boostEntries.size} repost${if (boostEntries.size != 1) "s" else ""}", + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + + HorizontalDivider() + + boostEntries.take(10).forEach { entry -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier.clickable { + onDismiss() + onNavigateToProfile(entry.pubKey) + }, + ) { + UserAvatar(userHex = entry.pubKey, pictureUrl = entry.pictureUrl, size = 24.dp) + Text(entry.name, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface) + } + } + if (boostEntries.size > 10) { + Text( + text = "and ${boostEntries.size - 10} more...", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } + } +} + /** * Fetches metadata for multiple users in a single subscription. */ @@ -744,6 +920,8 @@ fun NoteActionsRow( isBookmarked: Boolean = false, bookmarkList: BookmarkListEvent? = null, onBookmarkChanged: (BookmarkListEvent) -> Unit = {}, + onNavigateToThread: (String) -> Unit = {}, + onNavigateToProfile: (String) -> Unit = {}, ) { var isLiked by remember { mutableStateOf(false) } var isReposted by remember { mutableStateOf(false) } @@ -757,6 +935,9 @@ fun NoteActionsRow( // Mutually exclusive popup state var activePopup by remember { mutableStateOf(ActivePopup.None) } + // Quote compose state + var quoteEvent by remember { mutableStateOf(null) } + Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -846,7 +1027,9 @@ fun NoteActionsRow( ReactionsPopup( note = note, localCache = localCache, + relayManager = relayManager, onDismiss = { activePopup = ActivePopup.None }, + onNavigateToProfile = onNavigateToProfile, ) } @@ -908,7 +1091,11 @@ fun NoteActionsRow( } } }, - onLongClick = { /* no long-press action for repost */ }, + onLongClick = { + if (note != null) { + activePopup = ActivePopup.Boosts + } + }, indication = ripple(bounded = false, radius = 16.dp), interactionSource = remember { MutableInteractionSource() }, ).onPointerEvent(PointerEventType.Press) { pointerEvent -> @@ -957,12 +1144,22 @@ fun NoteActionsRow( text = { Text("Quote") }, onClick = { activePopup = ActivePopup.None - // Copy note link to clipboard for quoting - val noteLink = "nostr:${NNote.create(event.id)}" - copyToClipboard(noteLink) + quoteEvent = event }, ) } + + // Boosts popup (long-press) + if (activePopup is ActivePopup.Boosts && note != null) { + BoostsPopup( + note = note, + localCache = localCache, + relayManager = relayManager, + onDismiss = { activePopup = ActivePopup.None }, + onNavigateToThread = onNavigateToThread, + onNavigateToProfile = onNavigateToProfile, + ) + } } if (localRepostCount > 0) { Text( @@ -1026,7 +1223,9 @@ fun NoteActionsRow( ZapReceiptsPopup( note = note, localCache = localCache, + relayManager = relayManager, onDismiss = { activePopup = ActivePopup.None }, + onNavigateToProfile = onNavigateToProfile, ) } } @@ -1194,6 +1393,16 @@ fun NoteActionsRow( onDismiss = { showZapReceiptsDialog = false }, ) } + + // Quote compose dialog + if (quoteEvent != null) { + ComposeNoteDialog( + onDismiss = { quoteEvent = null }, + relayManager = relayManager, + account = account, + quoteOf = quoteEvent, + ) + } } /** diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt index d67732731e..c1878269a0 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.richtext.UrlParser import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState @@ -62,12 +63,18 @@ import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache import com.vitorpamplona.amethyst.desktop.feeds.DesktopThreadFilter import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator +import com.vitorpamplona.amethyst.desktop.subscriptions.FilterBuilders +import com.vitorpamplona.amethyst.desktop.subscriptions.SubscriptionConfig import com.vitorpamplona.amethyst.desktop.subscriptions.createNoteSubscription import com.vitorpamplona.amethyst.desktop.subscriptions.createThreadRepliesSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.generateSubId import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.desktop.ui.media.LightboxOverlay import com.vitorpamplona.amethyst.desktop.viewmodels.DesktopFeedViewModel import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip19Bech32.Nip19Parser +import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent +import com.vitorpamplona.quartz.nip19Bech32.entities.NNote /** * Desktop Thread Screen - displays a note and all its replies in a thread view. @@ -177,6 +184,38 @@ fun ThreadScreen( } } + // Fetch quoted notes referenced in thread content + val quotedNoteIds = + remember(threadNotes) { + threadNotes + .mapNotNull { it.event } + .flatMap { event -> + UrlParser() + .parseValidUrls(event.content) + .bech32s + .mapNotNull { bech32 -> + when (val entity = Nip19Parser.uriToRoute(bech32)?.entity) { + is NNote -> entity.hex + is NEvent -> entity.hex + else -> null + } + } + }.filter { localCache.getNoteIfExists(it)?.event == null } + .distinct() + } + + rememberSubscription(connectedRelays, quotedNoteIds, relayManager = relayManager) { + if (connectedRelays.isEmpty() || quotedNoteIds.isEmpty()) return@rememberSubscription null + SubscriptionConfig( + subId = generateSubId("thread-quoted"), + filters = listOf(FilterBuilders.byIds(quotedNoteIds)), + relays = connectedRelays, + onEvent = { event, _, relay, _ -> + subscriptionsCoordinator?.consumeEvent(event, relay) + }, + ) + } + // Calculate reply level for a note based on e-tags fun calculateLevel(note: Note): Int { val event = note.event ?: return 1 From 862dce27fe279db02f0838f7e84f8d08102ab2f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 16:23:34 +0000 Subject: [PATCH 19/21] fix(blossom-bridge): always use URL sha, ignore imeta `x` On resizing CDNs the imeta `x` (post-resize hash) can differ from the `ox` (original hash) embedded in the URL. The bridge previously preferred `explicitHash` over the URL's sha for "authoritative casing", but the upstream file on `xs` is named after the URL's sha, not the imeta hash. For URLs like https://image.nostr.build/.png with imeta x= the cache would request /.png and 404 on miss. Always use the sha parsed from the URL path; drop the explicitHash parameter. `extractSha256FromUrlPath` already lowercases, so the casing concern is moot. --- .../commons/richtext/MediaUrlContentExt.kt | 20 +++++++------------ .../richtext/MediaUrlContentExtTest.kt | 15 ++++++++++++++ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt index 29d750d34e..8d64214427 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExt.kt @@ -42,7 +42,6 @@ fun MediaUrlContent.toCoilModel(useLocalBlossomBridge: Boolean): String = bridgeUrl( url = url, useBridge = useLocalBlossomBridge, - explicitHash = hash, mimeType = mimeType, authorPubKey = authorPubKey, skipBridge = this is MediaUrlVideo && isLiveStream, @@ -95,7 +94,6 @@ const val DEFAULT_LOCAL_CACHE_BASE = "http://127.0.0.1:24242" private fun bridgeUrl( url: String, useBridge: Boolean, - explicitHash: String?, mimeType: String?, authorPubKey: String?, skipBridge: Boolean, @@ -105,19 +103,15 @@ private fun bridgeUrl( if (!url.startsWith("http://", ignoreCase = true) && !url.startsWith("https://", ignoreCase = true)) return url // The local Blossom cache fetches `/.` on miss per BUD-01, - // which only works when the upstream URL is itself BUD-01 layout. For - // non-BUD-01 URLs (e.g. https://i.nostr.build/M5AwJ.gif) the imeta `x` - // hash identifies the blob but the upstream server doesn't host it at - // /., so trusting only `explicitHash` would point the cache - // at a 404. Require the sha to be in the URL path before bridging. - val urlSha = extractSha256FromUrlPath(url) ?: return url - - // Prefer the imeta hash when it's a valid sha256 (authoritative casing), - // otherwise fall back to what was parsed from the URL. - val sha = explicitHash?.lowercase()?.takeIf { sha256HexRegex.matches(it) } ?: urlSha + // which only works when the upstream URL is itself BUD-01 layout — the + // file at `/.` is the one named in the URL path, not the + // imeta `x` hash (which on resizing CDNs may identify a different blob + // than the URL filename, e.g. `x` = post-resize, `ox` = original). + // Always use the URL's sha; never trust the imeta override. + val sha = extractSha256FromUrlPath(url) ?: return url val ext = guessExtension(url, mimeType) - val serverBase = extractServerBase(url, urlSha) ?: return url + val serverBase = extractServerBase(url, sha) ?: return url val authors = authorPubKey diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt index 84dcf80c83..16e4427cac 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt +++ b/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaUrlContentExtTest.kt @@ -255,4 +255,19 @@ class MediaUrlContentExtTest { val image = MediaUrlImage(url = url, hash = null) assertEquals(url, image.toCoilModel(useLocalBlossomBridge = true)) } + + @Test + fun bridgeOnUsesUrlShaNotImetaWhenTheyDiffer() { + // On resizing CDNs the imeta `x` (post-resize) can differ from the + // `ox` (original) embedded in the URL. The upstream file is named + // after the URL's sha, so the cache request must use that — using + // the imeta `x` would point xs= at a non-existent path on miss. + val urlSha = "f24026b7281e598973a775adefb1b9a13b9f037a94ac98dd48ccc91b83f4b7b3" + val imetaX = "6932a918de1bfae3bf6611794ff54dd677013d22b760a9212117a0bd9079badf" + val image = MediaUrlImage(url = "https://image.nostr.build/$urlSha.png", hash = imetaX) + assertEquals( + "blossom:$urlSha.png?xs=https://image.nostr.build", + image.toCoilModel(useLocalBlossomBridge = true), + ) + } } From a699920e962ededbad9797568db2d6ec20d29117 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 22 May 2026 22:06:54 +0000 Subject: [PATCH 20/21] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 295 +++++++++--------- .../src/main/res/values-de-rDE/strings.xml | 289 +++++++++-------- .../src/main/res/values-es-rES/strings.xml | 62 ++++ .../src/main/res/values-es-rMX/strings.xml | 62 ++++ .../src/main/res/values-es-rUS/strings.xml | 62 ++++ .../src/main/res/values-hu-rHU/strings.xml | 27 +- .../src/main/res/values-pl-rPL/strings.xml | 9 + .../src/main/res/values-pt-rBR/strings.xml | 282 ++++++++--------- .../src/main/res/values-sv-rSE/strings.xml | 283 ++++++++--------- .../src/main/res/values-zh-rCN/strings.xml | 13 + 10 files changed, 789 insertions(+), 595 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 3e488ce306..8f751e8640 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -562,6 +562,7 @@ Vyberte, které z odznaků, které jste obdrželi, se zobrazí na vašem profilu. Zatím jste neobdrželi žádné odznaky. Obrázky + Kalendáře Krátká videa Veřejné chaty Seznamy sledování @@ -796,6 +797,10 @@ Pokročilé: zadejte podrobnosti připojení ručně Rychlé částky Zap Zobrazí se při stisknutí tlačítka zap. Klepnutím na částku ji odeberete. Pokud ponecháte prázdné, otevře se pokaždé dialog pro zadání částky. + Rychlé částky on-chain zapů + On-chain zapy platí poplatky těžařům, takže částky jsou obvykle vyšší než u Lightningu. Klepnutím na částku ji odstraníte. + Nová on-chain částka v sats + Odeslat on-chain Soukromí Zap Určuje, jak je zobrazena vaše identita při odesílání zapsu. Připojit peněženku @@ -1606,6 +1611,8 @@ Globální Krátké Obrázky + Kalendáře + Seznamy kalendářů Šachy Peněženka Zůstatek @@ -1626,6 +1633,7 @@ Vytváření faktury… Kopírovat fakturu Zatím žádné transakce + Žádné transakce neodpovídají tomuto filtru Načítání… Přijato Odesláno @@ -1654,6 +1662,15 @@ Neplatné URI NWC připojení Posunout nahoru Posunout dolů + On-chain transakce + Pro tento účet není k dispozici žádná on-chain adresa. + Není nakonfigurován žádný on-chain backend. + Čeká + Veřejné + Tato peněženka je veřejná + Vaše Taproot adresa je odvozena z vašeho veřejného klíče Nostr, takže kdokoli, kdo zná váš npub, může vidět zůstatek této peněženky a historii transakcí na blockchainu.\n\nAbyste zachovali soukromí, vkládejte do této peněženky a vybírejte z ní prostředky z nesoukromých účtů, jako jsou burzy. Nikdy nemíchejte tyto prostředky se svými studenými peněženkami a zacházejte s nimi jako s penězi, které můžete ztratit, protože kdokoli, kdo má pod kontrolou váš nsec, je může utratit. + Rozumím + Zkopírovat adresu Bezpečnostní filtry Importovat sledované Nový příspěvek @@ -1666,6 +1683,128 @@ Nová zap anketa Nová běžná anketa Nový obrázek + Nová událost kalendáře + Upravit událost kalendáře + Uzamčeno během úprav — změna by vytvořila novou událost. + Nový kalendář + Upravit kalendář + Události v tomto kalendáři (%1$d) + Zatím jste nevytvořili žádné události. + Kanál + Měsíc + Týden + Den + Nadcházející + Minulé + Z vybraného kanálu zatím nejsou žádné nadcházející ani minulé události. + Zatím žádné kalendářové seznamy. + Váš kalendář je prázdný + Události sdílené lidmi, které sledujete, se zobrazí zde. Klepnutím na tlačítko + vytvořte vlastní. + Zatím žádné seznamy + Spojte události dohromady — sérii setkání, sekci konference, plán vašeho týmu. Klepnutím na + vytvořte nový. + Nic naplánováno + V tento den nejsou žádné události. Klepnutím na + přidejte. + Tento týden nic + V tomto týdnu nejsou žádné události. + Název + Shrnutí + Místo + URL obrázku + Celodenní událost + Začátek + Konec + Hashtagy (oddělené čárkou) + Vybrat datum + Vybrat čas + Název a začátek jsou povinné. + Konec musí být po začátku. + Předchozí měsíc + Další měsíc + Předchozí týden + Další týden + Předchozí den + Další den + V tento den nejsou žádné události + Žádné události + Pokračuje + Den %1$d z %2$d + Přidat do kalendáře v telefonu + Skočit na dnešek + + %1$s, %2$d událost + %1$s, %2$d události + %1$s, %2$d událostí + %1$s, %2$d událostí + + %1$s, žádné události + dnes + vybráno + Vytvořit novou událost kalendáře + Vytvořit nový kalendář + Zobrazit možnosti vytvoření + (bez názvu) + Celodenní + ✓ Účastním se + \? Možná + ✗ Nemůžu + Název + Popis + Název je povinný. + + %1$d událost + %1$d události + %1$d událostí + %1$d událostí + + V tomto kalendáři zatím nejsou žádné události. + Účastním se + Možná + Nemůžu + RSVP (%1$d) + Zatím žádné RSVP. + Účastníci (%1$d) + V kalendářích (%1$d) + Zatím není součástí žádného kalendáře. + Načítání události… + Probíhá nyní + %1$s · končí %2$s + Sdílet událost kalendáře + Exportovat do kalendáře (.ics) + Připomenutí kalendáře + Upozornění, když je událost, které se účastníte, blízko začátku. + Událost kalendáře + + Začíná za %1$d minutu + Začíná za %1$d minuty + Začíná za %1$d minut + Začíná za %1$d minut + + Přidat do jednoho z vašich kalendářů + Přidat do kalendáře + Zatím jste nevytvořili žádný kalendář. + Smazat kalendář + Smazat tento kalendář? + Kalendář bude odstraněn. Události v něm nebudou smazány. + Vybrat obrázek + Nahrání obrázku selhalo + Vybraný obrázek se nepodařilo nahrát. Zkuste to znovu nebo vložte URL. + Účastníci (%1$d) + Hledat jméno, npub nebo nip-05 + Zadejte platný npub… nebo 64znakový hex pubkey. + Odebrat účastníka + Připomenutí kalendáře + Posílat připomenutí + Oznámení se spustí, když je událost, které se účastníte, blízko začátku. + Doba předstihu připomenutí + Kolik minut před událostí chcete být upozorněni. + Sdílet jako Nostr odkaz + Sdílet odkaz na kalendář + Všechny kalendáře + Zobrazit události z… + Zatím jste nevytvořili žádný kalendář. + Otevřít v mapách + Otevřít odkaz + Detail události Nové krátké video Nové dlouhé video Název @@ -1689,6 +1828,7 @@ Zvýšit nebo citovat Olajkovat Zap + Čeká na potvrzení Změnit rychlé reakce Spodní navigační lišta Přetažením změníte pořadí. Přepnutím položku přidáte nebo odeberete ze spodní lišty. S nulovým počtem položek bude lišta skryta. @@ -2040,6 +2180,9 @@ Odebrat uživatele ze seznamu Balíček Sledování Členové + Seznam sledování (%1$d): + Seznam sledování + Seznam sledování (%1$d) Metadata seznamu sledování Metadata seznamů sledování mohou vidět všichni na Nostr. Pouze vaši soukromí členové jsou šifrovaní. Metadata sady doporučení @@ -2647,156 +2790,4 @@ Nowhere Zpráva Nowhere Umění Nowhere Fórum - - %1$d min - %1$d min - %1$d min - %1$d min - - Kalendáře - Celodenní - Pokračuje - (bez názvu) - Žádné události - V tento den nejsou žádné události - Skočit na dnešek - Další den - Další týden - Další měsíc - Předchozí den - Předchozí týden - Předchozí měsíc - Den - Týden - Měsíc - Kanál - Minulé - Nadcházející - Z vybraného kanálu zatím nejsou žádné nadcházející ani minulé události. - Váš kalendář je prázdný - Události sdílené lidmi, které sledujete, se zobrazí zde. Klepnutím na tlačítko + vytvořte vlastní. - Nic naplánováno - V tento den nejsou žádné události. Klepnutím na + přidejte. - Tento týden nic - V tomto týdnu nejsou žádné události. - Zatím žádné kalendářové seznamy. - Zatím žádné seznamy - Spojte události dohromady — sérii setkání, sekci konference, plán vašeho týmu. Klepnutím na + vytvořte nový. - vybráno - dnes - Den %1$d z %2$d - Zobrazit možnosti vytvoření - Vytvořit novou událost kalendáře - Vytvořit nový kalendář - Nový kalendář - Nová událost kalendáře - Upravit kalendář - Upravit událost kalendáře - Název - Popis - Název je povinný. - V tomto kalendáři zatím nejsou žádné události. - Zatím jste nevytvořili žádné události. - Události v tomto kalendáři (%1$d) - Smazat kalendář - Smazat tento kalendář? - Kalendář bude odstraněn. Události v něm nebudou smazány. - Název - Shrnutí - Místo - Začátek - Konec - Konec musí být po začátku. - Celodenní událost - Uzamčeno během úprav — změna by vytvořila novou událost. - Hashtagy (oddělené čárkou) - URL obrázku - Vybrat datum - Vybrat čas - Vybrat obrázek - Nahrání obrázku selhalo - Vybraný obrázek se nepodařilo nahrát. Zkuste to znovu nebo vložte URL. - Načítání události… - Název a začátek jsou povinné. - V kalendářích (%1$d) - Zatím není součástí žádného kalendáře. - Účastníci (%1$d) - Účastníci (%1$d) - Hledat jméno, npub nebo nip-05 - Zadejte platný npub… nebo 64znakový hex pubkey. - Odebrat účastníka - Všechny kalendáře - Zatím jste nevytvořili žádný kalendář. - Zobrazit události z… - Přidat do kalendáře - Přidat do jednoho z vašich kalendářů - Zatím jste nevytvořili žádný kalendář. - Přidat do kalendáře v telefonu - Sdílet jako Nostr odkaz - Sdílet odkaz na kalendář - Exportovat do kalendáře (.ics) - Sdílet událost kalendáře - Otevřít v mapách - Otevřít odkaz - Probíhá nyní - %1$s · končí %2$s - RSVP (%1$d) - Účastním se - ✓ Účastním se - Možná - \? Možná - Nemůžu - ✗ Nemůžu - Zatím žádné RSVP. - calendar_reminders - Připomenutí kalendáře - Upozornění, když je událost, které se účastníte, blízko začátku. - Událost kalendáře - Připomenutí kalendáře - Posílat připomenutí - Oznámení se spustí, když je událost, které se účastníte, blízko začátku. - Doba předstihu připomenutí - Kolik minut před událostí chcete být upozorněni. - Kalendáře - Seznamy kalendářů - Detail události - On-chain Bitcoin zap - Čeká na potvrzení - Nová on-chain částka v sats - Rychlé částky on-chain zapů - On-chain zapy platí poplatky těžařům, takže částky jsou obvykle vyšší než u Lightningu. Klepnutím na částku ji odstraníte. - Odeslat on-chain - Žádné transakce neodpovídají tomuto filtru - Zkopírovat adresu - Pro tento účet není k dispozici žádná on-chain adresa. - Není nakonfigurován žádný on-chain backend. - Čeká - Veřejné - Tato peněženka je veřejná - Vaše Taproot adresa je odvozena z vašeho veřejného klíče Nostr, takže kdokoli, kdo zná váš npub, může vidět zůstatek této peněženky a historii transakcí na blockchainu.\n\nAbyste zachovali soukromí, vkládejte do této peněženky a vybírejte z ní prostředky z nesoukromých účtů, jako jsou burzy. Nikdy nemíchejte tyto prostředky se svými studenými peněženkami a zacházejte s nimi jako s penězi, které můžete ztratit, protože kdokoli, kdo má pod kontrolou váš nsec, je může utratit. - Rozumím - On-chain transakce - Nowhere Drop - - %1$d událost - %1$d události - %1$d událostí - %1$d událostí - - - %1$s, %2$d událost - %1$s, %2$d události - %1$s, %2$d událostí - %1$s, %2$d událostí - - %1$s, žádné události - - Začíná za %1$d minutu - Začíná za %1$d minuty - Začíná za %1$d minut - Začíná za %1$d minut - - Seznam sledování - Seznam sledování (%1$d) - Seznam sledování (%1$d): diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index e2c1b6d217..cd69d1a59f 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -556,6 +556,7 @@ anz der Bedingungen ist erforderlich Wähle aus, welche deiner erhaltenen Abzeichen auf deinem Profil erscheinen sollen. Du hast noch keine Abzeichen erhalten. Bilder + Kalender Kurzvideos Öffentliche Chats Follow-Pakete @@ -788,6 +789,10 @@ anz der Bedingungen ist erforderlich Erweitert: Verbindungsdetails manuell eingeben Schnelle Zap-Beträge Wird beim Drücken der Zap-Schaltfläche angezeigt. Tippe auf einen Betrag, um ihn zu entfernen. Wenn du es leer lässt, wird jedes Mal ein Dialog zur Eingabe eines Betrags geöffnet. + Schnelle On-Chain-Zap-Beträge + On-Chain-Zaps zahlen Miner-Gebühren, daher sind die Beträge meist größer als bei Lightning. Tippe auf einen Betrag, um ihn zu entfernen. + Neuer On-Chain-Betrag in Sats + Stattdessen On-Chain senden Zap-Datenschutz Legt fest, wie deine Identität beim Senden eines Zaps angezeigt wird. Wallet verbinden @@ -1597,6 +1602,8 @@ anz der Bedingungen ist erforderlich Global Kurzfilme Bilder + Kalender + Kalenderlisten Schach Wallet Guthaben @@ -1617,6 +1624,7 @@ anz der Bedingungen ist erforderlich Rechnung wird erstellt… Rechnung kopieren Noch keine Transaktionen + Keine Transaktionen entsprechen diesem Filter Wird geladen… Empfangen Gesendet @@ -1645,6 +1653,15 @@ anz der Bedingungen ist erforderlich Ungültige NWC-Verbindungs-URI Nach oben Nach unten + On-Chain-Transaktionen + Keine On-Chain-Adresse für dieses Konto verfügbar. + Kein On-Chain-Backend konfiguriert. + Ausstehend + Öffentlich + Diese Wallet ist öffentlich + Deine Taproot-Adresse wird aus deinem öffentlichen Nostr-Schlüssel abgeleitet, daher kann jeder, der deinen npub kennt, den Saldo und die Transaktionshistorie dieser Wallet in der Blockchain einsehen.\n\nUm deine Privatsphäre zu wahren, lade diese Wallet von nicht-privaten Konten (z. B. Börsen) auf und sende Mittel auch nur an solche Konten zurück. Vermische diese Mittel niemals mit deinen Cold Wallets und behandle sie wie Geld, das du verlieren kannst, da jeder, der die Kontrolle über deinen nsec hat, die Mittel ausgeben kann. + Verstanden + Adresse kopieren Sicherheitsfilter Folgeliste importieren Neuer Beitrag @@ -1657,6 +1674,124 @@ anz der Bedingungen ist erforderlich Neue Zap-Umfrage Neue reguläre Umfrage Neues Bild + Neuer Kalendertermin + Kalendertermin bearbeiten + Während der Bearbeitung gesperrt — eine Änderung würde stattdessen einen neuen Termin erstellen. + Neuer Kalender + Kalender bearbeiten + Termine in diesem Kalender (%1$d) + Du hast noch keine Kalendertermine erstellt. + Monat + Woche + Tag + Bevorstehend + Vergangen + Noch keine bevorstehenden oder vergangenen Kalendertermine aus deinem ausgewählten Feed. + Noch keine Kalenderlisten. + Dein Kalender ist leer + Termine, die von Personen geteilt werden, denen du folgst, erscheinen hier. Tippe auf +, um eigene zu erstellen. + Noch keine Listen + Gruppiere Termine — eine Meetup-Reihe, ein Konferenz-Track, die Roadmap deines Teams. Tippe auf +, um eine zu erstellen. + Nichts geplant + Keine Termine an diesem Tag. Tippe auf +, um einen hinzuzufügen. + Nichts diese Woche + Keine Termine in dieser Woche. + Titel + Zusammenfassung + Ort + Bild-URL + Ganztägiger Termin + Beginnt + Endet + Hashtags (durch Komma getrennt) + Datum wählen + Zeit wählen + Titel und Start sind erforderlich. + Ende muss nach Beginn sein. + Vorheriger Monat + Nächster Monat + Vorherige Woche + Nächste Woche + Vorheriger Tag + Nächster Tag + Keine Termine an diesem Tag + Keine Termine + Geht weiter + Tag %1$d von %2$d + Zum Telefonkalender hinzufügen + Zu heute springen + + %1$s, %2$d Termin + %1$s, %2$d Termine + + %1$s, keine Termine + heute + ausgewählt + Neuen Kalendertermin erstellen + Neuen Kalender erstellen + Erstellungsoptionen anzeigen + (ohne Titel) + Ganztägig + ✓ Komme + \? Vielleicht + ✗ Komme nicht + Titel + Beschreibung + Ein Titel ist erforderlich. + + %1$d Termin + %1$d Termine + + Noch keine Termine in diesem Kalender. + Komme + Vielleicht + Komme nicht + Noch keine Antworten. + Teilnehmer (%1$d) + In Kalendern (%1$d) + Noch nicht Teil eines Kalenders. + Termin wird geladen… + Findet jetzt statt + %1$s · endet %2$s + Kalendertermin teilen + In Kalender exportieren (.ics) + Kalendererinnerungen + Hinweis, wenn ein Termin, an dem du teilnimmst, bald beginnt. + Kalendertermin + + Beginnt in %1$d Minute + Beginnt in %1$d Minuten + + Zu einem deiner Kalender hinzufügen + Zu einem Kalender hinzufügen + Du hast noch keine Kalender erstellt. + Kalender löschen + Diesen Kalender löschen? + Die Kalenderliste wird entfernt. Termine darin werden nicht gelöscht. + Bild wählen + Bild-Upload fehlgeschlagen + Das gewählte Bild konnte nicht hochgeladen werden. Versuche es erneut oder füge eine URL ein. + Teilnehmer (%1$d) + Name, npub oder nip-05 suchen + Gib einen gültigen npub… oder 64-stelligen hex pubkey ein. + Teilnehmer entfernen + Kalendererinnerungen + Erinnerungen senden + Eine Benachrichtigung wird angezeigt, wenn ein Termin, an dem du teilnimmst, bald beginnt. + Vorlaufzeit der Erinnerung + Wie viele Minuten vor dem Termin du benachrichtigt werden möchtest. + + %1$d Min. + %1$d Min. + + Als Nostr-Link teilen + Kalender-Link teilen + Alle Kalender + Termine anzeigen aus… + Du hast noch keine Kalender erstellt. + In Karten öffnen + Link öffnen + Termindetails Neues kurzes Video Neues langes Video Titel @@ -1680,6 +1815,8 @@ anz der Bedingungen ist erforderlich Boosten oder Zitieren Gefällt mir Zap + On-Chain-Bitcoin-Zap + Wartet auf Bestätigung Schnelle Reaktionen ändern Untere Navigationsleiste Zum Umordnen ziehen. Zum Hinzufügen oder Entfernen eines Elements aus der unteren Leiste umschalten. Mit null Elementen wird die untere Leiste ausgeblendet. @@ -2029,6 +2166,9 @@ anz der Bedingungen ist erforderlich Benutzer aus der Liste entfernen Folge Paket Mitglieder + Folgenliste (%1$d Nutzer): + Folgenliste + Folgenliste (%1$d) Metadaten der Follow-Liste Metadaten von Follow-Listen können von allen auf Nostr gesehen werden. Nur deine privaten Mitglieder sind verschlüsselt. Metadaten des Empfehlungspakets @@ -2621,153 +2761,4 @@ anz der Bedingungen ist erforderlich Nowhere Shop Nowhere Nachricht Nowhere Kunst - - %1$d Min. - %1$d Min. - - Kalender - Ganztägig - Geht weiter - (ohne Titel) - Keine Termine - Keine Termine an diesem Tag - Zu heute springen - Nächster Tag - Nächste Woche - Nächster Monat - Vorheriger Tag - Vorherige Woche - Vorheriger Monat - Tag - Woche - Monat - Feed - Vergangen - Bevorstehend - Noch keine bevorstehenden oder vergangenen Kalendertermine aus deinem ausgewählten Feed. - Dein Kalender ist leer - Termine, die von Personen geteilt werden, denen du folgst, erscheinen hier. Tippe auf +, um eigene zu erstellen. - Nichts geplant - Keine Termine an diesem Tag. Tippe auf +, um einen hinzuzufügen. - Nichts diese Woche - Keine Termine in dieser Woche. - Noch keine Kalenderlisten. - Noch keine Listen - Gruppiere Termine — eine Meetup-Reihe, ein Konferenz-Track, die Roadmap deines Teams. Tippe auf +, um eine zu erstellen. - ausgewählt - heute - Tag %1$d von %2$d - Erstellungsoptionen anzeigen - Neuen Kalendertermin erstellen - Neuen Kalender erstellen - Neuer Kalender - Neuer Kalendertermin - Kalender bearbeiten - Kalendertermin bearbeiten - Titel - Beschreibung - Ein Titel ist erforderlich. - Noch keine Termine in diesem Kalender. - Du hast noch keine Kalendertermine erstellt. - Termine in diesem Kalender (%1$d) - Kalender löschen - Diesen Kalender löschen? - Die Kalenderliste wird entfernt. Termine darin werden nicht gelöscht. - Titel - Zusammenfassung - Ort - Beginnt - Endet - Ende muss nach Beginn sein. - Ganztägiger Termin - Während der Bearbeitung gesperrt — eine Änderung würde stattdessen einen neuen Termin erstellen. - Hashtags (durch Komma getrennt) - Bild-URL - Datum wählen - Zeit wählen - Bild wählen - Bild-Upload fehlgeschlagen - Das gewählte Bild konnte nicht hochgeladen werden. Versuche es erneut oder füge eine URL ein. - Termin wird geladen… - Titel und Start sind erforderlich. - In Kalendern (%1$d) - Noch nicht Teil eines Kalenders. - Teilnehmer (%1$d) - Teilnehmer (%1$d) - Name, npub oder nip-05 suchen - Gib einen gültigen npub… oder 64-stelligen hex pubkey ein. - Teilnehmer entfernen - Alle Kalender - Du hast noch keine Kalender erstellt. - Termine anzeigen aus… - Zu einem Kalender hinzufügen - Zu einem deiner Kalender hinzufügen - Du hast noch keine Kalender erstellt. - Zum Telefonkalender hinzufügen - Als Nostr-Link teilen - Kalender-Link teilen - In Kalender exportieren (.ics) - Kalendertermin teilen - In Karten öffnen - Link öffnen - Findet jetzt statt - %1$s · endet %2$s - RSVPs (%1$d) - Komme - ✓ Komme - Vielleicht - \? Vielleicht - Komme nicht - ✗ Komme nicht - Noch keine Antworten. - calendar_reminders - Kalendererinnerungen - Hinweis, wenn ein Termin, an dem du teilnimmst, bald beginnt. - Kalendertermin - Kalendererinnerungen - Erinnerungen senden - Eine Benachrichtigung wird angezeigt, wenn ein Termin, an dem du teilnimmst, bald beginnt. - Vorlaufzeit der Erinnerung - Wie viele Minuten vor dem Termin du benachrichtigt werden möchtest. - Kalender - Kalenderlisten - Termindetails - On-Chain-Bitcoin-Zap - Wartet auf Bestätigung - Neuer On-Chain-Betrag in Sats - Schnelle On-Chain-Zap-Beträge - On-Chain-Zaps zahlen Miner-Gebühren, daher sind die Beträge meist größer als bei Lightning. Tippe auf einen Betrag, um ihn zu entfernen. - Stattdessen On-Chain senden - Keine Transaktionen entsprechen diesem Filter - Adresse kopieren - Keine On-Chain-Adresse für dieses Konto verfügbar. - Kein On-Chain-Backend konfiguriert. - Ausstehend - Öffentlich - Diese Wallet ist öffentlich - Deine Taproot-Adresse wird aus deinem öffentlichen Nostr-Schlüssel abgeleitet, daher kann jeder, der deinen npub kennt, den Saldo und die Transaktionshistorie dieser Wallet in der Blockchain einsehen.\n\nUm deine Privatsphäre zu wahren, lade diese Wallet von nicht-privaten Konten (z. B. Börsen) auf und sende Mittel auch nur an solche Konten zurück. Vermische diese Mittel niemals mit deinen Cold Wallets und behandle sie wie Geld, das du verlieren kannst, da jeder, der die Kontrolle über deinen nsec hat, die Mittel ausgeben kann. - Verstanden - On-Chain-Transaktionen - Nowhere Drop - - %1$d Termin - %1$d Termine - - - %1$s, %2$d Termin - %1$s, %2$d Termine - - %1$s, keine Termine - - Beginnt in %1$d Minute - Beginnt in %1$d Minuten - - Folgenliste - Folgenliste (%1$d) - Folgenliste (%1$d Nutzer): - Links - Issues - Nowhere Event - Nowhere Forum - Nowhere Petition diff --git a/amethyst/src/main/res/values-es-rES/strings.xml b/amethyst/src/main/res/values-es-rES/strings.xml index a4c3d8e31c..7923c6eaf5 100644 --- a/amethyst/src/main/res/values-es-rES/strings.xml +++ b/amethyst/src/main/res/values-es-rES/strings.xml @@ -8,6 +8,10 @@ Mostrar de todos modos Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas Post marcado como inapropiado por + + Esta publicación tiene más de %1$d hashtag + Esta publicación tiene más de %1$d hashtags + El evento se está cargando o no se puede encontrar en la lista de relés 👀 Imagen del canal @@ -30,6 +34,7 @@ Copiar texto Copiar PubKey del usuario Copiar ID de la nota + Copiar JSON sin procesar Transmisión Poner marca de tiempo Marca de tiempo: confirmaciones pendientes @@ -49,6 +54,10 @@ Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones. Falta la configuración de zaps por defecto. Mantén pulsado unos segundos para cambiarla + zapeó %1$s sats + Anónimo + está en una incursión + creó un clip Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder enviar zaps. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder seguir a otros usuarios. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios. @@ -153,6 +162,8 @@ Pronombres Dirección LN Dirección LN (antigua) + Los relés de origen pueden ser obsoletos + Aviso de relé obsoleto Guardar en el teléfono Guardar en la galería Imagen guardada en la galería @@ -161,6 +172,8 @@ Error al guardar la imagen Video guardado en la galería de videos del teléfono Error al guardar el video + PDF guardado en Descargas/Amethyst + Error al guardar el PDF Cargar imagen Cargar archivo Hacer una foto @@ -270,6 +283,8 @@ Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros y, por lo tanto, los mensajes del chat desaparecen con el tiempo Chat público + Grupo MLS + Aún no hay mensajes Metadatos públicos del chat Los chats públicos son visibles para todos en Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. @@ -334,6 +349,11 @@ "<Unable to decrypt private message>\n\nTe citaron en una conversación privada o encriptada entre %1$s y %2$s." Agregar cuenta nueva Cuentas + Navegar + + Tablones + Crear + Sistema Seleccionar cuenta Agregar cuenta nueva Cuenta activa @@ -356,6 +376,8 @@ Bloquear Eliminar Bloquear + Silenciar hilo + Reactivar hilo Reportar Eliminar No mostrar de nuevo @@ -385,9 +407,49 @@ Mover todo a marcadores nuevos Los marcadores se migraron correctamente Borradores + Publicaciones programadas + Programar + Hora programada + Las publicaciones se realizan en ~15 minutos a partir de la hora programada. + Elegir hora programada + Programación para… + Se publica en %1$s + Se venció hace %1$s + Hora + Programar publicación + Cancelar programación + Las notificaciones siempre activas están deshabilitadas + Las publicaciones programadas pueden no realizarse hasta que vuelvas a abrir la aplicación. Habilita la activación en \"Configuración → Preferencias de interfaz de usuario\" para garantizar una programación en segundo plano fiable. + Las publicaciones programadas pueden no realizarse hasta que vuelvas a abrir la aplicación. Las publicaciones programadas de otras cuentas no se activarán mientras esta cuenta esté activa. Habilita la activación en \"Configuración → Preferencias de interfaz de usuario\" para garantizar una programación en segundo plano fiable. + En 1 hora + Mañana a las 9 a. m. + El próximo lunes a las 9 a. m. + ¿Habilitar notificaciones siempre activas? + Las publicaciones programadas solo se realizan de forma fiable cuando las notificaciones siempre activas están habilitadas. De lo contrario, es posible que no se activen hasta que vuelvas a abrir la app. + Abrir configuración + Continuar de todos modos + %1$s · en %2$s + %1$s · hace %2$s + Mañana + Sesión cerrada + + Sesión cerrada · %d publicación programada eliminada + Sesión cerrada · %d publicaciones programadas eliminadas + + Se realizó la publicación programada + Fallo en la publicación programada + Publicaciones programadas + Notificaciones cuando una publicación programada se realiza o no. + Enviar ahora Encuestas Abiertas Cerradas + Guardar límites + Usuarios bloqueados + Estas claves públicas no pueden publicar en la comunidad. + Agregar un usuario bloqueado + Nombre, npub, o NIP-05 + Portal de red de confianza (opcional) Imágenes Cortos Videos diff --git a/amethyst/src/main/res/values-es-rMX/strings.xml b/amethyst/src/main/res/values-es-rMX/strings.xml index e5c28cb02a..be12d8f895 100644 --- a/amethyst/src/main/res/values-es-rMX/strings.xml +++ b/amethyst/src/main/res/values-es-rMX/strings.xml @@ -8,6 +8,10 @@ Mostrar de todos modos Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas La publicación fue reportada por + + Esta publicación tiene más de %1$d hashtag + Esta publicación tiene más de %1$d hashtags + El evento se está cargando o no se puede encontrar en la lista de relés 👀 Imagen del canal @@ -30,6 +34,7 @@ Copiar texto Copiar llave pública del usuario Copiar ID de la nota + Copiar JSON sin procesar Transmisión Poner marca de tiempo Marca de tiempo: confirmaciones pendientes @@ -49,6 +54,10 @@ Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones. No has configurado la cantidad de zaps. Mantén presionado el botón para cambiarla. + zapeó %1$s sats + Anónimo + está en una incursión + creó un clip Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder enviar zaps. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder seguir a otros usuarios. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios. @@ -152,6 +161,8 @@ Pronombres Dirección de Lightning URL de Lightning (obsoleta) + Los relés de origen pueden ser obsoletos + Aviso de relé obsoleto Guardar en el teléfono Guardar en la galería Imagen guardada en la galería @@ -160,6 +171,8 @@ Error al guardar la imagen Video guardado en la galería de videos del teléfono Error al guardar el video + PDF guardado en Descargas/Amethyst + Error al guardar el PDF Subir imagen Subir archivo Tomar una foto @@ -266,6 +279,8 @@ Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros y, por lo tanto, los mensajes del chat desaparecen con el tiempo Chat público + Grupo MLS + Aún no hay mensajes Metadatos públicos del chat Los chats públicos son visibles para todos en Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. @@ -330,6 +345,11 @@ "<Unable to decrypt private message>\n\nTe citaron en una conversación privada o encriptada entre %1$s y %2$s." Agregar cuenta nueva Cuentas + Navegar + + Tablones + Crear + Sistema Seleccionar cuenta Agregar cuenta nueva Cuenta activa @@ -352,6 +372,8 @@ Bloquear Eliminar Bloquear + Silenciar hilo + Reactivar hilo Reportar Eliminar No mostrar de nuevo @@ -381,9 +403,49 @@ Mover todo a marcadores nuevos Los marcadores se migraron correctamente Borradores + Publicaciones programadas + Programar + Hora programada + Las publicaciones se realizan en ~15 minutos a partir de la hora programada. + Elegir hora programada + Programación para… + Se publica en %1$s + Se venció hace %1$s + Hora + Programar publicación + Cancelar programación + Las notificaciones siempre activas están deshabilitadas + Las publicaciones programadas pueden no realizarse hasta que vuelvas a abrir la aplicación. Habilita la activación en \"Configuración → Preferencias de interfaz de usuario\" para garantizar una programación en segundo plano fiable. + Las publicaciones programadas pueden no realizarse hasta que vuelvas a abrir la aplicación. Las publicaciones programadas de otras cuentas no se activarán mientras esta cuenta esté activa. Habilita la activación en \"Configuración → Preferencias de interfaz de usuario\" para garantizar una programación en segundo plano fiable. + En 1 hora + Mañana a las 9 a. m. + El próximo lunes a las 9 a. m. + ¿Habilitar notificaciones siempre activas? + Las publicaciones programadas solo se realizan de forma fiable cuando las notificaciones siempre activas están habilitadas. De lo contrario, es posible que no se activen hasta que vuelvas a abrir la app. + Abrir configuración + Continuar de todos modos + %1$s · en %2$s + %1$s · hace %2$s + Mañana + Sesión cerrada + + Sesión cerrada · %d publicación programada eliminada + Sesión cerrada · %d publicaciones programadas eliminadas + + Se realizó la publicación programada + Fallo en la publicación programada + Publicaciones programadas + Notificaciones cuando una publicación programada se realiza o no. + Enviar ahora Encuestas Abiertas Cerradas + Guardar límites + Usuarios bloqueados + Estas claves públicas no pueden publicar en la comunidad. + Agregar un usuario bloqueado + Nombre, npub, o NIP-05 + Portal de red de confianza (opcional) Imágenes Cortos Videos diff --git a/amethyst/src/main/res/values-es-rUS/strings.xml b/amethyst/src/main/res/values-es-rUS/strings.xml index 52dad5014a..9bd1f0e5e2 100644 --- a/amethyst/src/main/res/values-es-rUS/strings.xml +++ b/amethyst/src/main/res/values-es-rUS/strings.xml @@ -8,6 +8,10 @@ Mostrar de todos modos Esta publicación se ocultó porque menciona tus usuarios o palabras ocultas La publicación fue reportada por + + Esta publicación tiene más de %1$d hashtag + Esta publicación tiene más de %1$d hashtags + El evento se está cargando o no se puede encontrar en la lista de relés 👀 Imagen del canal @@ -30,6 +34,7 @@ Copiar texto Copiar ID del autor Copiar ID de la nota + Copiar JSON sin procesar Transmisión Poner marca de tiempo Marca de tiempo: confirmaciones pendientes @@ -49,6 +54,10 @@ Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder impulsar publicaciones Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para indicar que te gustan las publicaciones. No has configurado la cantidad de zaps. Mantén presionado el botón para cambiarla. + zapeó %1$s sats + Anónimo + está en una incursión + creó un clip Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder enviar zaps. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder seguir a otros usuarios. Estás usando una clave pública, que es solo de lectura. Inicia sesión con una clave privada para poder dejar de seguir a otros usuarios. @@ -152,6 +161,8 @@ Pronombres Dirección de Lightning URL de Lightning (obsoleta) + Los relés de origen pueden ser obsoletos + Aviso de relé obsoleto Guardar en el teléfono Guardar en la galería Imagen guardada en la galería @@ -160,6 +171,8 @@ Error al guardar la imagen Video guardado en la galería de videos del teléfono Error al guardar el video + PDF guardado en Descargas/Amethyst + Error al guardar el PDF Subir imagen Subir archivo Tomar una foto @@ -266,6 +279,8 @@ Son ideales para comunidades abiertas en torno a temas específicos. Algunos de estos grupos son efímeros y, por lo tanto, los mensajes del chat desaparecen con el tiempo Chat público + Grupo MLS + Aún no hay mensajes Metadatos públicos del chat Los chats públicos son visibles para todos en Nostr y cualquiera puede participar en ellos. Son ideales para comunidades abiertas alrededor de temas específicos. @@ -330,6 +345,11 @@ "<No se pudo desencriptar el mensaje privado>\n\nTe citaron en una conversación privada o encriptada entre %1$s y %2$s." Agregar cuenta nueva Cuentas + Navegar + + Tablones + Crear + Sistema Seleccionar cuenta Agregar cuenta nueva Cuenta activa @@ -352,6 +372,8 @@ Bloquear Eliminar Bloquear + Silenciar hilo + Reactivar hilo Reportar Eliminar No mostrar de nuevo @@ -381,9 +403,49 @@ Mover todo a marcadores nuevos Los marcadores se migraron correctamente Borradores + Publicaciones programadas + Programar + Hora programada + Las publicaciones se realizan en ~15 minutos a partir de la hora programada. + Elegir hora programada + Programación para… + Se publica en %1$s + Se venció hace %1$s + Hora + Programar publicación + Cancelar programación + Las notificaciones siempre activas están deshabilitadas + Las publicaciones programadas pueden no realizarse hasta que vuelvas a abrir la aplicación. Habilita la activación en \"Configuración → Preferencias de interfaz de usuario\" para garantizar una programación en segundo plano fiable. + Las publicaciones programadas pueden no realizarse hasta que vuelvas a abrir la aplicación. Las publicaciones programadas de otras cuentas no se activarán mientras esta cuenta esté activa. Habilita la activación en \"Configuración → Preferencias de interfaz de usuario\" para garantizar una programación en segundo plano fiable. + En 1 hora + Mañana a las 9 a. m. + El próximo lunes a las 9 a. m. + ¿Habilitar notificaciones siempre activas? + Las publicaciones programadas solo se realizan de forma fiable cuando las notificaciones siempre activas están habilitadas. De lo contrario, es posible que no se activen hasta que vuelvas a abrir la app. + Abrir configuración + Continuar de todos modos + %1$s · en %2$s + %1$s · hace %2$s + Mañana + Sesión cerrada + + Sesión cerrada · %d publicación programada eliminada + Sesión cerrada · %d publicaciones programadas eliminadas + + Se realizó la publicación programada + Fallo en la publicación programada + Publicaciones programadas + Notificaciones cuando una publicación programada se realiza o no. + Enviar ahora Encuestas Abiertas Cerradas + Guardar límites + Usuarios bloqueados + Estas claves públicas no pueden publicar en la comunidad. + Agregar un usuario bloqueado + Nombre, npub, o NIP-05 + Portal de red de confianza (opcional) Imágenes Cortos Videos diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index 5f3a40e45e..96415b3792 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -16,7 +16,7 @@ 👀 Csatorna profilképe A hivatkozott esemény nem található - Nem sikerült az üzenetet visszafejteni + Nem sikerült visszafejteni az üzenetet Csoport profilképe Szókimondó tartalom Átjátszóval kapcsolatos megjegyzések @@ -785,6 +785,10 @@ Speciális: kapcsolat részleteinek kézi megadása Gyors Zap-összegek A zap gomb megnyomásakor jelenik meg. Érintse meg az összeget, hogy eltávolítsa. Ha üresen hagyja, akkor minden alkalommal megnyílik a párbeszédablak az összeg beírásához. + Gyors láncon belüli Zap összegek + A láncon belüli Zapek bányászdíjat fizetnek, ezért az összegek általában nagyobbak, mint Lightning esetén. Koppintson egy összegre a eltávolításához. + Új láncon belüli összeg Satoshiban + Küldés inkább láncon belül Zap adatvédelem Állítsa be, hogy hogyan jelenjen meg a személyazonossága a zap elküldésekor. Pénztárca összekapcsolása @@ -1454,8 +1458,8 @@ Profilkép Profilképek megjelenítése Válasszon egy lehetőséget - Sikertelen számla-kifizetés - Sikertelen pénzkivétel a pénztárcából + Nem sikerült kifizetni a számlát + Nem sikerült a pénzkivétel a pénztárcából Nem sikerült beállítani a Wallet Connectet Hiba a NIP-47 kapcsolati karakterlánc elemzésekor. Ellenőrizze, hogy a pénztárca-szolgáltatónál a következő helyes-e: %1$s. Hiba: %2$s Hiba a NIP-47 kapcsolati karakterlánc elemzésekor. Ellenőrizze, hogy a pénztárca-szolgáltatónál a következő helyes-e: %1$s. @@ -1653,6 +1657,11 @@ Ehhez a fiókhoz nincs elérhető láncon belüli cím. Nincs beállítva lánc-háttérprogram. Függőben + Nyilvános + Ez a pénztárca nyilvános + A Taproot-cím az Ön Nostr nyilvános kulcsából van levezetve, ezért bárki, aki ismeri az npub-ot, láthatja a pénztárca egyenlegét és tranzakciótörténetét a blokkláncon.\n\nAz adatvédelem megőrzése érdekében ezt a pénztárcát kizárólag nem privát számlákról, például kriptovaluta-váltókról töltse fel és ürítse ki. Soha ne keverje ezeket az összegeket a hideg pénztárcákkal, és olyan pénzként kezelje őket, amelyet elveszíthet, mivel bárki, aki hozzáfér az nsec-hez, elköltheti. + Megértettem + Cím másolása Biztonsági szűrők Követettek importálása Új bejegyzés @@ -1716,6 +1725,7 @@ %1$s, %2$d esemény %1$s, %2$d esemény + %1$s, nincsenek események ma kiválasztva Új naptáresemény létrehozása @@ -1729,6 +1739,10 @@ Cím Leírás A cím megadása kötelező. + + %1$d esemény + %1$d esemény + Ebben a naptárban még nincsenek események. Ott leszek Talán @@ -1747,6 +1761,10 @@ Naptár-emlékeztetők Értesítés, ha egy esemény, amin Ön részt vesz, hamarosan elkezdődik. Naptáresemény + + %1$d perc múlva kezdődik + %1$d perc múlva kezdődik + Vegye fel az egyik naptárba Hozzáadás egy naptárhoz Ön még nem hozott létre egyetlen naptárat sem. @@ -2153,6 +2171,9 @@ Felhasználó törlése a listáról Követési csomag Tagok + A követési lista %1$d felhasználót tartalmaz: + Követési lista + Követési lista (%1$d) Követési lista metaadatai A követési listák metaadatai a Nostr-on bárki számára láthatók. Csak a privát tagok adatai vannak titkosítva. Követési csomag metaadatai diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index b3e0eaac67..5f9ee02a4d 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -797,6 +797,10 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Zaawansowane: wprowadź szczegóły połączenia ręcznie Kwoty Quick Zap Wyświetlane po naciśnięciu przycisku zap. Dotknij kwoty, aby ją usunąć. Jeśli pozostawisz to pole puste, za każdym razem otworzy się okno dialogowe umożliwiające wprowadzenie kwoty. + Szybkie kwoty zapoów On-chain + Transakcje on-chain są obciążone opłatami minerów, więc kwoty są zazwyczaj wyższe niż w sieci Lightning. Kliknij kwotę, aby ją usunąć. + Nowa kwota on-chain w satoszach + Zamiast tego wyślij on-chain Prywatność Zap Kontroluje sposób wyświetlania Twojej tożsamości podczas wysyłania zapa. Podłącz portfel @@ -1669,6 +1673,7 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Ten portfel jest publiczny Twój adres Taproot jest generowany na podstawie Twojego klucza publicznego Nostr, więc każdy, kto zna Twój klucz npub, może sprawdzić saldo tego portfela oraz historię transakcji w łańcuchu bloków.\n\nAby zachować prywatność, zasilaj ten portfel i wypłacaj z niego środki wyłącznie z kont publicznych, takich jak giełdy. Nigdy nie mieszaj tych środków ze środkami z portfeli offline i traktuj je jako pieniądze, które możesz stracić, ponieważ każdy, kto ma dostęp do Twojego klucza nsec, może je wydać. Rozumiem + Skopiuj adres Filtry bezpieczeństwa Import Obserwowanych Nowy post @@ -1734,6 +1739,7 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest %1$s, wydarzeń %2$d %1$s, %2$d wydarzeń + %1$s, brak wydarzeń dziś zaznaczone Utwórz nowe wydarzenie kalendarza @@ -2173,6 +2179,9 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest Usuń użytkownika z listy Kategoria profili Uczestnicy + Lista obserwowanych zawiera %1$d użytkownika(ów): + Lista obserwowanych + Lista obserwowanych (%1$d) Metadane listy profili Metadane listy obserwowanych są widoczne dla wszystkich użytkowników w sieci Nostr. Tylko Twoi uczestnicy prywatni są zaszyfrowani. Metadane pakietu subskrybentów diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 27b2e99712..955e8373ed 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -550,6 +550,7 @@ Escolha quais dos selos que você recebeu aparecerão no seu perfil. Você ainda não recebeu nenhum selo. Imagens + Calendários Curtas Chats públicos Pacotes de seguidos @@ -782,6 +783,10 @@ Avançado: inserir detalhes de conexão manualmente Valores Rápidos de Zap Exibido ao pressionar o botão de zap. Toque em um valor para removê-lo. Se deixar vazio, abrirá o diálogo para inserir um valor todas as vezes. + Valores rápidos de zap on-chain + Zaps on-chain pagam taxas de mineradores, por isso os valores costumam ser maiores que no Lightning. Toque num valor para removê-lo. + Novo valor on-chain em sats + Enviar on-chain em vez disso Privacidade do Zap Controla como sua identidade é exibida ao enviar um Zap. Conectar Carteira @@ -1592,6 +1597,8 @@ Global Vídeos Curtos Imagens + Calendários + Listas de calendário Xadrez Carteira Saldo @@ -1612,6 +1619,7 @@ Criando fatura… Copiar Fatura Nenhuma transação ainda + Nenhuma transação corresponde a este filtro Carregando… Recebido Enviado @@ -1640,6 +1648,15 @@ URI de conexão NWC inválida Mover para cima Mover para baixo + Transações on-chain + Nenhum endereço on-chain disponível para esta conta. + Nenhum backend on-chain configurado. + Pendente + Pública + Esta carteira é pública + Seu endereço Taproot é derivado da sua chave pública Nostr, então qualquer pessoa que conheça seu npub pode ver o saldo e o histórico de transações desta carteira no blockchain.\n\nPara preservar sua privacidade, abasteça esta carteira a partir de contas não privadas (como corretoras) e envie os fundos de volta para elas. Nunca misture esses fundos com suas carteiras frias e trate-os como dinheiro que você pode perder, pois qualquer um que controle seu nsec pode gastar os fundos. + Entendi + Copiar endereço Filtros de Segurança Importar Seguidos Novo Post @@ -1652,6 +1669,120 @@ Nova Enquete Zap Nova Enquete Regular Nova Foto + Novo evento de calendário + Editar evento de calendário + Bloqueado durante edição — alterar isto criaria um novo evento. + Novo calendário + Editar calendário + Eventos neste calendário (%1$d) + Você ainda não criou nenhum evento de calendário. + Mês + Semana + Dia + Próximos + Passados + Ainda não há eventos de calendário (futuros ou passados) do feed selecionado. + Ainda não há listas de calendário. + Seu calendário está vazio + Eventos compartilhados por pessoas que você segue aparecem aqui. Toque no botão + para criar os seus. + Ainda não há listas + Agrupe eventos — uma série de encontros, uma trilha de conferência, o roteiro da sua equipe. Toque em + para criar uma. + Nada agendado + Sem eventos neste dia. Toque em + para adicionar um. + Nada esta semana + Nenhum evento nesta semana. + Título + Resumo + Local + URL da imagem + Evento de dia inteiro + Começa + Termina + Hashtags (separadas por vírgula) + Escolher data + Escolher hora + Título e início são obrigatórios. + O término deve ser após o início. + Mês anterior + Próximo mês + Semana anterior + Próxima semana + Dia anterior + Próximo dia + Sem eventos neste dia + Sem eventos + Continua + Dia %1$d de %2$d + Adicionar ao calendário do telefone + Ir para hoje + + %1$s, %2$d evento + %1$s, %2$d eventos + + %1$s, sem eventos + hoje + selecionado + Criar um novo evento de calendário + Criar um novo calendário + Mostrar opções de criação + (sem título) + Dia inteiro + ✓ Vou + \? Talvez + ✗ Não vou + Título + Descrição + Um título é obrigatório. + + %1$d evento + %1$d eventos + + Nenhum evento neste calendário ainda. + Vou + Talvez + Não vou + Sem RSVPs ainda. + Participantes (%1$d) + Em calendários (%1$d) + Ainda não faz parte de nenhum calendário. + Carregando evento… + Acontecendo agora + %1$s · termina %2$s + Compartilhar evento de calendário + Exportar para calendário (.ics) + Lembretes de calendário + Aviso quando um evento do qual você participa está prestes a começar. + Evento de calendário + + Começa em %1$d minuto + Começa em %1$d minutos + + Adicionar a um dos seus calendários + Adicionar a um calendário + Você ainda não criou nenhum calendário. + Excluir calendário + Excluir este calendário? + O calendário será removido. Os eventos dentro dele não serão excluídos. + Escolher imagem + Falha no envio da imagem + A imagem escolhida não pôde ser enviada. Tente novamente ou cole uma URL. + Participantes (%1$d) + Buscar nome, npub ou nip-05 + Insira um npub… válido ou pubkey hex de 64 caracteres. + Remover participante + Lembretes de calendário + Enviar lembretes + Uma notificação é disparada quando um evento do qual você participa está prestes a começar. + Antecedência do lembrete + Quantos minutos antes do evento você quer ser notificado. + Compartilhar como link Nostr + Compartilhar link do calendário + Todos os calendários + Mostrar eventos de… + Você ainda não criou nenhum calendário. + Abrir no mapa + Abrir link + Detalhes do evento Novo Vídeo Curto Novo Vídeo Longo Título @@ -1675,6 +1806,8 @@ Impulsionar ou Citar Gostar Zap + Zap Bitcoin on-chain + Aguardando confirmação Mudar reações rápidas Barra de navegação inferior Arraste para reordenar. Alterne para adicionar ou remover um item da barra inferior. Com zero itens, a barra inferior fica oculta. @@ -2024,6 +2157,9 @@ Remover usuário da lista Pacote Seguir Membros + Lista de seguidos contendo %1$d usuário(s): + Lista de seguidos + Lista de seguidos (%1$d) Metadados da lista de seguidores Os metadados das listas de seguidores podem ser vistos por qualquer pessoa no Nostr. Apenas os membros privados são criptografados. Metadados do pacote de recomendações @@ -2619,150 +2755,4 @@ Nowhere Mensagem Nowhere Arte Nowhere Fórum - - %1$d min - %1$d min - - Calendários - Dia inteiro - Continua - (sem título) - Sem eventos - Sem eventos neste dia - Ir para hoje - Próximo dia - Próxima semana - Próximo mês - Dia anterior - Semana anterior - Mês anterior - Dia - Semana - Mês - Feed - Passados - Próximos - Ainda não há eventos de calendário (futuros ou passados) do feed selecionado. - Seu calendário está vazio - Eventos compartilhados por pessoas que você segue aparecem aqui. Toque no botão + para criar os seus. - Nada agendado - Sem eventos neste dia. Toque em + para adicionar um. - Nada esta semana - Nenhum evento nesta semana. - Ainda não há listas de calendário. - Ainda não há listas - Agrupe eventos — uma série de encontros, uma trilha de conferência, o roteiro da sua equipe. Toque em + para criar uma. - selecionado - hoje - Dia %1$d de %2$d - Mostrar opções de criação - Criar um novo evento de calendário - Criar um novo calendário - Novo calendário - Novo evento de calendário - Editar calendário - Editar evento de calendário - Título - Descrição - Um título é obrigatório. - Nenhum evento neste calendário ainda. - Você ainda não criou nenhum evento de calendário. - Eventos neste calendário (%1$d) - Excluir calendário - Excluir este calendário? - O calendário será removido. Os eventos dentro dele não serão excluídos. - Título - Resumo - Local - Começa - Termina - O término deve ser após o início. - Evento de dia inteiro - Bloqueado durante edição — alterar isto criaria um novo evento. - Hashtags (separadas por vírgula) - URL da imagem - Escolher data - Escolher hora - Escolher imagem - Falha no envio da imagem - A imagem escolhida não pôde ser enviada. Tente novamente ou cole uma URL. - Carregando evento… - Título e início são obrigatórios. - Em calendários (%1$d) - Ainda não faz parte de nenhum calendário. - Participantes (%1$d) - Participantes (%1$d) - Buscar nome, npub ou nip-05 - Insira um npub… válido ou pubkey hex de 64 caracteres. - Remover participante - Todos os calendários - Você ainda não criou nenhum calendário. - Mostrar eventos de… - Adicionar a um calendário - Adicionar a um dos seus calendários - Você ainda não criou nenhum calendário. - Adicionar ao calendário do telefone - Compartilhar como link Nostr - Compartilhar link do calendário - Exportar para calendário (.ics) - Compartilhar evento de calendário - Abrir no mapa - Abrir link - Acontecendo agora - %1$s · termina %2$s - RSVPs (%1$d) - Vou - ✓ Vou - Talvez - \? Talvez - Não vou - ✗ Não vou - Sem RSVPs ainda. - calendar_reminders - Lembretes de calendário - Aviso quando um evento do qual você participa está prestes a começar. - Evento de calendário - Lembretes de calendário - Enviar lembretes - Uma notificação é disparada quando um evento do qual você participa está prestes a começar. - Antecedência do lembrete - Quantos minutos antes do evento você quer ser notificado. - Calendários - Listas de calendário - Detalhes do evento - Zap Bitcoin on-chain - Aguardando confirmação - Novo valor on-chain em sats - Valores rápidos de zap on-chain - Zaps on-chain pagam taxas de mineradores, por isso os valores costumam ser maiores que no Lightning. Toque num valor para removê-lo. - Enviar on-chain em vez disso - Nenhuma transação corresponde a este filtro - Copiar endereço - Nenhum endereço on-chain disponível para esta conta. - Nenhum backend on-chain configurado. - Pendente - Pública - Esta carteira é pública - Seu endereço Taproot é derivado da sua chave pública Nostr, então qualquer pessoa que conheça seu npub pode ver o saldo e o histórico de transações desta carteira no blockchain.\n\nPara preservar sua privacidade, abasteça esta carteira a partir de contas não privadas (como corretoras) e envie os fundos de volta para elas. Nunca misture esses fundos com suas carteiras frias e trate-os como dinheiro que você pode perder, pois qualquer um que controle seu nsec pode gastar os fundos. - Entendi - Transações on-chain - Nowhere Drop - - %1$d evento - %1$d eventos - - - %1$s, %2$d evento - %1$s, %2$d eventos - - %1$s, sem eventos - - Começa em %1$d minuto - Começa em %1$d minutos - - Lista de seguidos - Lista de seguidos (%1$d) - Lista de seguidos contendo %1$d usuário(s): - Links - Issues diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index d467b17c42..010fb91c5a 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -550,6 +550,7 @@ Välj vilka av märkena du fått som ska visas på din profil. Du har inte fått några märken ännu. Bilder + Kalendrar Kortfilmer Offentliga chattar Följpaket @@ -782,6 +783,10 @@ Avancerat: ange anslutningsinformation manuellt Snabba Zap-belopp Visas när du trycker på Zap-knappen. Tryck på ett belopp för att ta bort det. Om du lämnar det tomt öppnas en dialog för att ange ett belopp varje gång. + Snabba on-chain-zapbelopp + On-chain-zappar betalar miner-avgifter, så beloppen är vanligtvis större än för Lightning. Tryck på ett belopp för att ta bort det. + Nytt on-chain-belopp i sats + Skicka on-chain istället Zap-sekretess Styr hur din identitet visas när du skickar ett Zap. Anslut plånbok @@ -1591,6 +1596,8 @@ Globalt Kortfilmer Bilder + Kalendrar + Kalenderlistor Schack Plånbok Saldo @@ -1611,6 +1618,7 @@ Skapar faktura… Kopiera faktura Inga transaktioner ännu + Inga transaktioner matchar detta filter Laddar… Mottagen Skickad @@ -1639,6 +1647,15 @@ Ogiltig URI för NWC-anslutning Flytta upp Flytta ner + On-chain-transaktioner + Ingen on-chain-adress tillgänglig för detta konto. + Ingen on-chain-backend är konfigurerad. + Väntar + Offentlig + Denna plånbok är offentlig + Din Taproot-adress härleds från din publika Nostr-nyckel, så alla som känner till din npub kan se denna plånboks saldo och transaktionshistorik på blockchainen.\n\nFör att bevara din integritet, fyll på denna plånbok från icke-privata konton (som börser) och skicka tillbaka medel dit. Blanda aldrig dessa medel med dina kalla plånböcker, och behandla dem som pengar du kan förlora, eftersom alla som har kontroll över din nsec kan spendera medlen. + Uppfattat + Kopiera adress Säkerhetsfilter Importera följare Nytt inlägg @@ -1651,6 +1668,122 @@ Ny zap-omröstning Ny vanlig omröstning Ny bild + Ny kalenderhändelse + Redigera kalenderhändelse + Låst under redigering — att ändra detta skulle skapa en ny händelse istället. + Ny kalender + Redigera kalender + Händelser i denna kalender (%1$d) + Du har inte skapat några kalenderhändelser än. + Flöde + Månad + Vecka + Dag + Kommande + Tidigare + Inga kommande eller tidigare kalenderhändelser från ditt valda flöde än. + Inga kalenderlistor än. + Din kalender är tom + Händelser delade av personer du följer dyker upp här. Tryck på +-knappen för att skapa egna. + Inga listor än + Gruppera händelser tillsammans — en träffserie, ett konferensspår, ditt teams färdplan. Tryck på + för att skapa en. + Inget schemalagt + Inga händelser denna dag. Tryck på + för att lägga till en. + Inget den här veckan + Inga händelser denna vecka. + Titel + Sammanfattning + Plats + Bild-URL + Heldagshändelse + Börjar + Slutar + Hashtaggar (kommaseparerade) + Välj datum + Välj tid + Titel och starttid krävs. + Slut måste vara efter start. + Föregående månad + Nästa månad + Föregående vecka + Nästa vecka + Föregående dag + Nästa dag + Inga händelser denna dag + Inga händelser + Fortsätter + Dag %1$d av %2$d + Lägg till i telefonens kalender + Hoppa till idag + + %1$s, %2$d händelse + %1$s, %2$d händelser + + %1$s, inga händelser + idag + vald + Skapa en ny kalenderhändelse + Skapa en ny kalender + Visa skapandealternativ + (utan titel) + Heldag + ✓ Kommer + \? Kanske + ✗ Kommer inte + Titel + Beskrivning + En titel krävs. + + %1$d händelse + %1$d händelser + + Inga händelser i denna kalender än. + Kommer + Kanske + Kommer inte + OSA (%1$d) + Inga OSA än. + Deltagare (%1$d) + I kalendrar (%1$d) + Inte del av någon kalender än. + Laddar händelse… + Pågår nu + %1$s · slutar %2$s + Dela kalenderhändelse + Exportera till kalender (.ics) + Kalenderpåminnelser + Påminnelse när en händelse du deltar i snart ska börja. + Kalenderhändelse + + Börjar om %1$d minut + Börjar om %1$d minuter + + Lägg till i en av dina kalendrar + Lägg till i en kalender + Du har inte skapat några kalendrar än. + Radera kalender + Radera denna kalender? + Kalenderlistan kommer att tas bort. Händelser inuti den raderas inte. + Välj bild + Bilduppladdning misslyckades + Den valda bilden kunde inte laddas upp. Försök igen eller klistra in en URL. + Deltagare (%1$d) + Sök namn, npub eller nip-05 + Ange en giltig npub… eller 64-teckens hex pubkey. + Ta bort deltagare + Kalenderpåminnelser + Skicka påminnelser + En notis visas när en händelse du deltar i snart ska börja. + Påminnelsetid + Hur många minuter före händelsen du vill bli notifierad. + Dela som Nostr-länk + Dela kalenderlänk + Alla kalendrar + Visa händelser från… + Du har inte skapat några kalendrar än. + Öppna i kartor + Öppna länk + Händelsedetaljer Ny kort video Ny lång video Titel @@ -1674,6 +1807,8 @@ Boosta eller citera Gilla Zap + On-chain Bitcoin-zap + Väntar på bekräftelse Ändra Snabba Reaktioner Nedre navigeringsfält Dra för att ändra ordning. Växla för att lägga till eller ta bort ett objekt från nedre fältet. Med noll objekt döljs det nedre fältet. @@ -2025,6 +2160,9 @@ Ta bort användare från listan Följpaket Medlemmar + Följlista som innehåller %1$d användare: + Följlista + Följlista (%1$d) Metadata för följlista Metadata för följlistor kan ses av alla på Nostr. Endast privata medlemmar är krypterade. Metadata för rekommendationspaket @@ -2619,149 +2757,4 @@ Nowhere Namninsamling Nowhere Meddelande Nowhere Konst - - %1$d min - %1$d min - - Kalendrar - Heldag - Fortsätter - (utan titel) - Inga händelser - Inga händelser denna dag - Hoppa till idag - Nästa dag - Nästa vecka - Nästa månad - Föregående dag - Föregående vecka - Föregående månad - Dag - Vecka - Månad - Flöde - Tidigare - Kommande - Inga kommande eller tidigare kalenderhändelser från ditt valda flöde än. - Din kalender är tom - Händelser delade av personer du följer dyker upp här. Tryck på +-knappen för att skapa egna. - Inget schemalagt - Inga händelser denna dag. Tryck på + för att lägga till en. - Inget den här veckan - Inga händelser denna vecka. - Inga kalenderlistor än. - Inga listor än - Gruppera händelser tillsammans — en träffserie, ett konferensspår, ditt teams färdplan. Tryck på + för att skapa en. - vald - idag - Dag %1$d av %2$d - Visa skapandealternativ - Skapa en ny kalenderhändelse - Skapa en ny kalender - Ny kalender - Ny kalenderhändelse - Redigera kalender - Redigera kalenderhändelse - Titel - Beskrivning - En titel krävs. - Inga händelser i denna kalender än. - Du har inte skapat några kalenderhändelser än. - Händelser i denna kalender (%1$d) - Radera kalender - Radera denna kalender? - Kalenderlistan kommer att tas bort. Händelser inuti den raderas inte. - Titel - Sammanfattning - Plats - Börjar - Slutar - Slut måste vara efter start. - Heldagshändelse - Låst under redigering — att ändra detta skulle skapa en ny händelse istället. - Hashtaggar (kommaseparerade) - Bild-URL - Välj datum - Välj tid - Välj bild - Bilduppladdning misslyckades - Den valda bilden kunde inte laddas upp. Försök igen eller klistra in en URL. - Laddar händelse… - Titel och starttid krävs. - I kalendrar (%1$d) - Inte del av någon kalender än. - Deltagare (%1$d) - Deltagare (%1$d) - Sök namn, npub eller nip-05 - Ange en giltig npub… eller 64-teckens hex pubkey. - Ta bort deltagare - Alla kalendrar - Du har inte skapat några kalendrar än. - Visa händelser från… - Lägg till i en kalender - Lägg till i en av dina kalendrar - Du har inte skapat några kalendrar än. - Lägg till i telefonens kalender - Dela som Nostr-länk - Dela kalenderlänk - Exportera till kalender (.ics) - Dela kalenderhändelse - Öppna i kartor - Öppna länk - Pågår nu - %1$s · slutar %2$s - OSA (%1$d) - Kommer - ✓ Kommer - Kanske - \? Kanske - Kommer inte - ✗ Kommer inte - Inga OSA än. - calendar_reminders - Kalenderpåminnelser - Påminnelse när en händelse du deltar i snart ska börja. - Kalenderhändelse - Kalenderpåminnelser - Skicka påminnelser - En notis visas när en händelse du deltar i snart ska börja. - Påminnelsetid - Hur många minuter före händelsen du vill bli notifierad. - Kalendrar - Kalenderlistor - Händelsedetaljer - On-chain Bitcoin-zap - Väntar på bekräftelse - Nytt on-chain-belopp i sats - Snabba on-chain-zapbelopp - On-chain-zappar betalar miner-avgifter, så beloppen är vanligtvis större än för Lightning. Tryck på ett belopp för att ta bort det. - Skicka on-chain istället - Inga transaktioner matchar detta filter - Kopiera adress - Ingen on-chain-adress tillgänglig för detta konto. - Ingen on-chain-backend är konfigurerad. - Väntar - Offentlig - Denna plånbok är offentlig - Din Taproot-adress härleds från din publika Nostr-nyckel, så alla som känner till din npub kan se denna plånboks saldo och transaktionshistorik på blockchainen.\n\nFör att bevara din integritet, fyll på denna plånbok från icke-privata konton (som börser) och skicka tillbaka medel dit. Blanda aldrig dessa medel med dina kalla plånböcker, och behandla dem som pengar du kan förlora, eftersom alla som har kontroll över din nsec kan spendera medlen. - Uppfattat - On-chain-transaktioner - Nowhere Drop - - %1$d händelse - %1$d händelser - - - %1$s, %2$d händelse - %1$s, %2$d händelser - - %1$s, inga händelser - - Börjar om %1$d minut - Börjar om %1$d minuter - - Följlista - Följlista (%1$d) - Följlista som innehåller %1$d användare: - Nowhere Forum diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index f7274f778b..1656bf9fde 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -778,6 +778,10 @@ 高级:手动输入连接详情 快速打闪金额 按下打闪按钮时显示。点击接即可删除它。 如果留空,每次都会打开对话框来插入金融。 + 链上快速打闪金额 + 链上打闪支付矿工费,所以金额通常大于 Lightning 网络。点击一个金额去掉它。 + 新的链上金额(sats) + 在链上发送 打闪隐私 控制发送打闪时如何显示您的身份。 连接钱包 @@ -1646,6 +1650,11 @@ 此账户没有链上地址可用。 没有配置链后端。 待确认 + 公开 + 这个钱包是公开的 + 您的Taproot地址衍生自您的Nostr公钥, 因此任何知道您的 npub的人都可以在区块链上看到这个钱包的余额和交易历史。\n\n为了保护您的隐私,这个钱包的资金流入和资金流出请使用非私密账户,例如交易所。 永远不要将这些资金与你的冷钱包混合,把它们当作你可能丢失的钱, 因为任何控制你 nsec 的人都可以花这些钱。 + 明白了 + 复制地址 安全滤镜 导入关注 新帖子 @@ -1708,6 +1717,7 @@ %1$s, %2$d 个活动 + %1$s,无活动 今天 已选中 创建新的日历活动 @@ -2150,6 +2160,9 @@ 从列表中删除用户 关注包 成员 + 包含 %1$d 个用户的关注列表: + 关注列表 + 关注列表(%1$d) 关注列表元数据 关注列表元数据可以被Nostr上的任何人看到。只有您的私人成员是加密的。 关注包元数据 From 585b28163a8ab865e78439cfb6efe0b930f0bcb4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Fri, 22 May 2026 18:25:07 -0400 Subject: [PATCH 21/21] Better rendering of Public Chats --- .../ui/components/SensitivityWarning.kt | 8 +- .../amethyst/ui/note/DisplayAuthorBanner.kt | 27 +++++- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 9 +- .../ui/note/elements/DefaultImageHeader.kt | 40 ++++----- .../RenderPublicChatChannelThumb.kt | 5 +- .../publicChats/PublicChatsFeedLoaded.kt | 84 +++++++++++++++---- .../nip28PublicChats/PublicChatListState.kt | 14 ++++ 7 files changed, 133 insertions(+), 54 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt index ddbb336cbc..fd8d7b7e29 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/SensitivityWarning.kt @@ -82,7 +82,13 @@ fun SensitivityWarning( accountViewModel: AccountViewModel, content: @Composable () -> Unit, ) { - note.event?.let { SensitivityWarning(it, accountViewModel, content) } + val noteEvent = note.event + + if (noteEvent == null) { + content() + } else { + SensitivityWarning(noteEvent, accountViewModel, content) + } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt index 337c19e403..67eb8ac256 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/DisplayAuthorBanner.kt @@ -21,23 +21,44 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote +import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.note.elements.BannerImage import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.SimpleImageBorder @Composable fun DisplayAuthorBanner( - note: Note, + baseNote: Note, accountViewModel: AccountViewModel, modifier: Modifier = SimpleImageBorder, ) { - WatchAuthor(note, accountViewModel) { + val noteAuthor = baseNote.author + if (noteAuthor != null) { BannerImage( - it, + noteAuthor, modifier, accountViewModel, ) + } else { + val authorState by observeNote(baseNote, accountViewModel) + CrossfadeIfEnabled(authorState.note.author, accountViewModel = accountViewModel) { author -> + if (author != null) { + BannerImage( + author, + modifier, + accountViewModel, + ) + } else { + BannerImage( + null as String?, + modifier, + accountViewModel, + ) + } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 28e04659bb..a7e17d4536 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -154,12 +154,11 @@ fun LoadPublicChatChannel( accountViewModel: AccountViewModel, content: @Composable (PublicChatChannel) -> Unit, ) { - val channel = - produceStateIfNotNull(accountViewModel.getPublicChatChannelIfExists(id), id) { - value = accountViewModel.checkGetOrCreatePublicChatChannel(id) - } + val channel by produceStateIfNotNull(accountViewModel.getPublicChatChannelIfExists(id), id) { + value = accountViewModel.checkGetOrCreatePublicChatChannel(id) + } - channel.value?.let { content(it) } + channel?.let { content(it) } } @Composable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt index dabcf9a69f..943c105290 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DefaultImageHeader.kt @@ -119,38 +119,32 @@ fun BannerImage( if (!banner.isNullOrBlank()) { MyAsyncImage( imageUrl = banner, - contentDescription = - stringRes( - R.string.preview_card_image_for, - banner, - ), + contentDescription = stringRes(R.string.preview_card_image_for, banner), contentScale = ContentScale.Crop, mainImageModifier = Modifier, loadedImageModifier = modifier, accountViewModel = accountViewModel, onLoadingBackground = { - Image( - painter = painterRes(R.drawable.profile_banner, 4), - contentDescription = stringRes(R.string.profile_banner), - contentScale = ContentScale.Crop, - modifier = modifier, - ) + DefaultProfileBanner(modifier, 4) }, onError = { - Image( - painter = painterRes(R.drawable.profile_banner, 4), - contentDescription = stringRes(R.string.profile_banner), - contentScale = ContentScale.Crop, - modifier = modifier, - ) + DefaultProfileBanner(modifier, 4) }, ) } else { - Image( - painter = painterRes(R.drawable.profile_banner, 5), - contentDescription = stringRes(R.string.profile_banner), - contentScale = ContentScale.Crop, - modifier = modifier, - ) + DefaultProfileBanner(modifier, 5) } } + +@Composable +fun DefaultProfileBanner( + modifier: Modifier, + sizeReference: Int, +) { + Image( + painter = painterRes(R.drawable.profile_banner, sizeReference), + contentDescription = stringRes(R.string.profile_banner), + contentScale = ContentScale.Crop, + modifier = modifier, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 2d2cc7f720..e5bab74702 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -68,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.ui.theme.RowColSpacing import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.grayText -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -81,8 +80,6 @@ fun RenderPublicChatChannelThumb( accountViewModel: AccountViewModel, nav: INav, ) { - val noteEvent = baseNote.event as? ChannelCreateEvent ?: return - LoadPublicChatChannel(baseNote.idHex, accountViewModel) { RenderPublicChatChannelThumb(baseNote = baseNote, channel = it, accountViewModel, nav) } @@ -96,7 +93,7 @@ fun RenderPublicChatChannelThumb( nav: INav, ) { val channelUpdates by observeChannel(channel, accountViewModel) - val publicChat = channelUpdates?.channel as PublicChatChannel + val publicChat = (channelUpdates?.channel as? PublicChatChannel) ?: channel val name = remember(channelUpdates) { publicChat.toBestDisplayName() } val description = remember(channelUpdates) { publicChat.summary()?.ifBlank { null } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt index b15269c53b..02e1a22882 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/publicChats/PublicChatsFeedLoaded.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.publicChats import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -45,15 +46,20 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState -import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.note.CheckHiddenFeedWatchBlockAndReport +import com.vitorpamplona.amethyst.ui.note.ClickableNote +import com.vitorpamplona.amethyst.ui.note.LongPressToQuickAction +import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent +import com.vitorpamplona.amethyst.ui.note.calculateBackgroundColor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.ChannelCardCompose +import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip28Chats.RenderPublicChatChannelThumb import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding -import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent +import com.vitorpamplona.amethyst.ui.theme.StdPadding @Composable fun PublicChatsFeedLoaded( @@ -66,12 +72,8 @@ fun PublicChatsFeedLoaded( val followedSet by accountViewModel.account.publicChatList.flowSet .collectAsStateWithLifecycle() - val pinned = - remember(followedSet) { - followedSet.mapNotNull { idHex -> - LocalCache.getNoteIfExists(idHex)?.takeIf { it.event is ChannelCreateEvent } - } - } + val pinned by accountViewModel.account.publicChatList.flowSetNote + .collectAsStateWithLifecycle() val unpinned = remember(items.list, followedSet) { @@ -91,7 +93,6 @@ fun PublicChatsFeedLoaded( items( pinned, key = { item -> "pinned-" + item.idHex }, - contentType = { item -> item.event?.kind ?: -1 }, ) { item -> PublicChatRow(item, pinned = true, accountViewModel, nav) } @@ -105,7 +106,6 @@ fun PublicChatsFeedLoaded( itemsIndexed( unpinned, key = { _, item -> item.idHex }, - contentType = { _, item -> item.event?.kind ?: -1 }, ) { _, item -> PublicChatRow(item, pinned = false, accountViewModel, nav) } @@ -114,20 +114,34 @@ fun PublicChatsFeedLoaded( @Composable private fun LazyItemScope.PublicChatRow( - item: Note, + baseNote: Note, pinned: Boolean, accountViewModel: AccountViewModel, nav: INav, ) { + val modifier = Modifier.fillMaxWidth() + Box(Modifier.fillMaxWidth().animateItem()) { - ChannelCardCompose( - baseNote = item, - routeForLastRead = "PublicChatsFeed", - modifier = Modifier.fillMaxWidth(), - forceEventKind = ChannelCreateEvent.KIND, + WatchNoteEvent( + baseNote = baseNote, accountViewModel = accountViewModel, - nav = nav, + onBlank = { + RenderChannel(baseNote, modifier, accountViewModel, nav) + }, + onNoteEventFound = { + CheckHiddenFeedWatchBlockAndReport( + note = baseNote, + modifier = modifier, + ignoreAllBlocksAndReports = false, + showHiddenWarning = false, + accountViewModel = accountViewModel, + nav = nav, + ) { _ -> + RenderChannel(baseNote, modifier, accountViewModel, nav) + } + }, ) + if (pinned) { PinBadge( modifier = @@ -143,6 +157,40 @@ private fun LazyItemScope.PublicChatRow( ) } +@Composable +private fun RenderChannel( + baseNote: Note, + modifier: Modifier, + accountViewModel: AccountViewModel, + nav: INav, +) { + LongPressToQuickAction(baseNote, accountViewModel, nav) { showPopup -> + ClickableNote( + baseNote = baseNote, + backgroundColor = + calculateBackgroundColor( + createdAt = baseNote.createdAt(), + routeForLastRead = "PublicChatsFeed", + parentBackgroundColor = null, + accountViewModel = accountViewModel, + ), + modifier = modifier, + accountViewModel = accountViewModel, + showPopup = showPopup, + nav = nav, + ) { + Column(StdPadding) { + SensitivityWarning( + note = baseNote, + accountViewModel = accountViewModel, + ) { + RenderPublicChatChannelThumb(baseNote, accountViewModel, nav) + } + } + } + } +} + @Composable private fun PinBadge(modifier: Modifier = Modifier) { Box( 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 2a676de5f9..3103bc76a7 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 @@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest @@ -96,6 +97,19 @@ class PublicChatListState( emptySet(), ) + val flowSetNote = + flowSet + .map { + it.mapNotNull { + cache.checkGetOrCreateNote(it) + } + }.flowOn(Dispatchers.IO) + .stateIn( + scope, + SharingStarted.Eagerly, + emptyList(), + ) + suspend fun follow(channel: PublicChatChannel): ChannelListEvent { val publicChatList = getChannelList()