From e874e302e06d14191d6ccf047b5e09f2e82dbc58 Mon Sep 17 00:00:00 2001 From: davotoula Date: Fri, 9 Jan 2026 11:04:34 +0100 Subject: [PATCH 01/15] update cz, pt, de, sv --- .../src/main/res/values-cs-rCZ/strings.xml | 49 +++++++++++++++++-- .../src/main/res/values-de-rDE/strings.xml | 47 +++++++++++++++++- .../src/main/res/values-pt-rBR/strings.xml | 49 +++++++++++++++++-- .../src/main/res/values-sv-rSE/strings.xml | 49 +++++++++++++++++-- 4 files changed, 183 insertions(+), 11 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index c32931f87d..0cae372a82 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -655,10 +655,8 @@ Předplatné Filtry Délka ID předplatného - Minimální předpona - Maximální počet značek události + Min. předpona Délka obsahu - Minimální PoW Ověření Platba Cashu Token @@ -1184,4 +1182,49 @@ Broadcast sady doporučení Smazat seznam Smazat sadu doporučení + Jméno (pro @tagování) + Moje @tag jméno + Oznámení relé + Duplicitní příspěvek + Chyba + Servisní klíč + Spuštěno %1$s + Spuštěno %1$s (%2$s) + Podporované GRASPy + Vstup + Předplatné + Publikování + Platby %1$s + Cílové publikum + Zásady & odkazy + Poplatky & platby + Témata + Všechny země + Všechny jazyky + Zásady ochrany soukromí + Podmínky & ujednání + N/A + Max. značek události + Max. délka obsahu + Zahazuje starší než + Přijímá až + %1$s v budoucnu + před %1$s + %1$s nul + %1$s bitů + Uchovávání událostí + Velikost obsahu + Připojení + Řízení přístupu + Min. obtížnost PoW + Vyžaduje se autentizace + Vyžaduje se platba + Max. délka zprávy + Max. odběrů + Max. filtrů na odběr + Max. limit (vracené události) + Výchozí limit (vracené události) + Max. délka SubID + Výchozí záložky + Vaše výchozí záložky, které mnoho klientů podporuje diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 2073348b2e..dc8aebca63 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -661,9 +661,7 @@ anz der Bedingungen ist erforderlich Filter Länge der Abonnement-ID Mindestpräfix - Maximale Anzahl von Ereignis-Tags Inhaltslänge - Mindest-PoW Authentifizierung Zahlung Cashu-Token @@ -1189,4 +1187,49 @@ anz der Bedingungen ist erforderlich Empfehlungspaket veröffentlichen Liste löschen Empfehlungspaket löschen + Name (zum @Taggen) + Mein @tag-Name + Relay-Hinweis + Doppelter Beitrag + Fehler + Service-Schlüssel + %1$s wird ausgeführt + %1$s wird ausgeführt (%2$s) + Unterstützte GRASPs + Zutritt + Abonnement + Veröffentlichung + Zahlungen %1$s + Zielgruppe + Richtlinien & Links + Gebühren & Zahlungen + Themen + Alle Länder + Alle Sprachen + Datenschutzerklärung + Allgemeine Geschäftsbedingungen + N/A + Max. Event-Tags + Max. Inhaltslänge + Verwirft älter als + Akzeptiert bis zu + %1$s in der Zukunft + vor %1$s + %1$s Nullen + %1$s Bits + Ereignis-Aufbewahrung + Inhaltsgröße + Verbindung + Zugriffskontrolle + Min. PoW-Schwierigkeit + Authentifizierung erforderlich + Zahlung erforderlich + Max. Nachrichtenlänge + Max. Abonnements + Max. Filter pro Abo + Max. Limit (zurückgegebene Ereignisse) + Standardlimit (zurückgegebene Ereignisse) + Max. SubID-Länge + Standard-Lesezeichen + Deine Standard-Lesezeichen, die viele Clients unterstützen diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 6cbc364bee..05c69d1a0d 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -655,10 +655,7 @@ Assinaturas Filtros Comprimento do ID da assinatura - Prefixo mínimo - Máximo de tags de evento Tamanho do conteúdo - PoW mínimo Autenticação Pagamento Token Cashu @@ -1184,4 +1181,50 @@ Transmitir pacote de recomendações Excluir lista Excluir pacote de recomendações + Nome (para @marcar) + Meu nome de @tag + Aviso do relay + Post duplicado + Erro + Chave de serviço + Executando %1$s + Executando %1$s (%2$s) + GRASPs compatíveis + Entrada + Assinatura + Publicação + Pagamentos %1$s + Público-alvo + Políticas & links + Taxas & pagamentos + Tópicos + Todos os países + Todos os idiomas + Política de privacidade + Termos & condições + N/A + Prefixo mín. + Máx. tags de evento + Máx. tamanho do conteúdo + Descarta mais antigos que + Aceita até + %1$s no futuro + há %1$s + %1$s zeros + %1$s bits + Retenção de eventos + Tamanho do conteúdo + Conectividade + Controle de acesso + Dificuldade mínima de PoW + Autenticação obrigatória + Pagamento obrigatório + Máx. tamanho da mensagem + Máx. inscrições + Máx. filtros por inscrição + Limite máx. (eventos retornados) + Limite padrão (eventos retornados) + Comprimento máx. do SubID + Marcadores padrão + Seus marcadores padrão que muitos clientes suportam diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 6939e8824c..f4646c2261 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -654,10 +654,7 @@ Prenumerationer Filter Längd på prenumerations-ID - Minsta prefix - Maximalt antal händelse-taggar Innehållslängd - Minsta PoW Autentisering Betalning Cashu Token @@ -1183,4 +1180,50 @@ Sänd rekommendationspaket Ta bort lista Ta bort rekommendationspaket + Namn (för @taggning) + Mitt @tag-namn + Relay-meddelande + Duplicerat inlägg + Fel + Service-nyckel + Kör %1$s + Kör %1$s (%2$s) + Stödda GRASPs + Inträde + Prenumeration + Publicering + Betalningar %1$s + Målgrupp + Policyer & länkar + Avgifter & betalningar + Ämnen + Alla länder + Alla språk + Integritetspolicy + Villkor & bestämmelser + N/A + Min. prefix + Max. eventtaggar + Max. innehållslängd + Kastar bort äldre än + Accepterar upp till + %1$s i framtiden + för %1$s sedan + %1$s nollor + %1$s bitar + Lagringstid för händelser + Innehållsstorlek + Anslutning + Åtkomstkontroll + Min. PoW-svårighet + Autentisering krävs + Betalning krävs + Max. meddelandelängd + Max. prenumerationer + Max. filter per prenumeration + Maxgräns (returnerade händelser) + Standardgräns (returnerade händelser) + Max. SubID-längd + Standardbokmärken + Dina standardbokmärken som många klienter stödjer From c7b408f79d7b25c46513502d52d5408a535e483d Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 9 Jan 2026 11:29:27 +0000 Subject: [PATCH 02/15] New Crowdin translations by GitHub Action --- .../src/main/res/values-cs-rCZ/strings.xml | 89 +++++++++--------- .../src/main/res/values-de-rDE/strings.xml | 89 +++++++++--------- .../src/main/res/values-hi-rIN/strings.xml | 46 +++++++++- .../src/main/res/values-pt-rBR/strings.xml | 89 +++++++++--------- .../src/main/res/values-sv-rSE/strings.xml | 91 +++++++++---------- .../src/main/res/values-zh-rCN/strings.xml | 43 +++++++++ 6 files changed, 264 insertions(+), 183 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 0cae372a82..8ff4d900ae 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -15,6 +15,8 @@ Nepodařilo se dešifrovat zprávu Obrázek skupiny Explicitní obsah + Oznámení relé + Duplicitní příspěvek Nevyžádaná pošta Počet spamových událostí z tohoto relé Zneužívání identity @@ -121,6 +123,7 @@ Adresa relé Příspěvky Byty + Chyba Chyby Počet chyb připojení v této relaci Domovský kanál @@ -132,6 +135,8 @@ Přidat uživatele Přidat přeposílání Jméno + Jméno (pro @tagování) + Moje @tag jméno Zobrazované jméno Moje zobrazované jméno Ostrich McAwesome @@ -358,6 +363,8 @@ Blokovat Manuální zap rozdělení Záložky + Výchozí záložky + Vaše výchozí záložky, které mnoho klientů podporuje Koncepty Soukromé záložky Veřejné záložky @@ -639,26 +646,63 @@ Množství bajtů, které bylo přijato z tohoto relé, včetně filtrů a událostí Při pokusu o získání informací z Relay se vyskytla chyba z %1$s Vlastník + Servisní klíč + Spuštěno %1$s + Spuštěno %1$s (%2$s) Verze Programové vybavení Kontakt Podporované NIPs + Podporované GRASPy Vstupné + Vstup + Předplatné + Publikování + Platby %1$s URL plateb + Cílové publikum + Zásady & odkazy + Poplatky & platby Omezení Země Jazyky Tagy + Témata + Všechny země + Všechny jazyky Politika příspěvků + Zásady ochrany soukromí + Podmínky & ujednání Chyby a upozornění z tohoto relé Délka zprávy Předplatné Filtry Délka ID předplatného Min. předpona + Max. značek události Délka obsahu + Max. délka obsahu + Zahazuje starší než + Přijímá až + %1$s v budoucnu + před %1$s + %1$s nul + %1$s bitů + Uchovávání událostí + Velikost obsahu + Připojení + Řízení přístupu + Min. obtížnost PoW Ověření + Vyžaduje se autentizace Platba + Vyžaduje se platba + Max. délka zprávy + Max. odběrů + Max. filtrů na odběr + Max. limit (vracené události) + Výchozí limit (vracené události) + Max. délka SubID Cashu Token Vyměnit Poslat do Zap peněženky @@ -1182,49 +1226,4 @@ Broadcast sady doporučení Smazat seznam Smazat sadu doporučení - Jméno (pro @tagování) - Moje @tag jméno - Oznámení relé - Duplicitní příspěvek - Chyba - Servisní klíč - Spuštěno %1$s - Spuštěno %1$s (%2$s) - Podporované GRASPy - Vstup - Předplatné - Publikování - Platby %1$s - Cílové publikum - Zásady & odkazy - Poplatky & platby - Témata - Všechny země - Všechny jazyky - Zásady ochrany soukromí - Podmínky & ujednání - N/A - Max. značek události - Max. délka obsahu - Zahazuje starší než - Přijímá až - %1$s v budoucnu - před %1$s - %1$s nul - %1$s bitů - Uchovávání událostí - Velikost obsahu - Připojení - Řízení přístupu - Min. obtížnost PoW - Vyžaduje se autentizace - Vyžaduje se platba - Max. délka zprávy - Max. odběrů - Max. filtrů na odběr - Max. limit (vracené události) - Výchozí limit (vracené události) - Max. délka SubID - Výchozí záložky - Vaše výchozí záložky, které mnoho klientů podporuje diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index dc8aebca63..f31726918b 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -15,6 +15,8 @@ Nachricht konnte nicht entschlüsselt werden Gruppenbild Anstößiger Inhalt + Relay-Hinweis + Doppelter Beitrag Spam Anzahl der Spam-Ereignisse von diesem Relais Vortäuschung @@ -121,6 +123,7 @@ Relay-Adresse Beiträge Bytes + Fehler Fehler Anzahl der Verbindungsfehler in dieser Sitzung Startseite @@ -132,6 +135,8 @@ Benutzer hinzufügen Relay hinzufügen Name + Name (zum @Taggen) + Mein @tag-Name Anzeigename Mein Anzeigename Ostrich McAwesome @@ -364,6 +369,8 @@ anz der Bedingungen ist erforderlich Blockieren Manuelle Zap-Splits Lesezeichen + Standard-Lesezeichen + Deine Standard-Lesezeichen, die viele Clients unterstützen Entwürfe Private Lesezeichen Öffentliche Lesezeichen @@ -644,26 +651,63 @@ anz der Bedingungen ist erforderlich Die Menge in Bytes, die von diesem Relais empfangen wurde, einschließlich Filter und Ereignisse Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten Inhaber + Service-Schlüssel + %1$s wird ausgeführt + %1$s wird ausgeführt (%2$s) Version Programme Kontakt Unterstützte NIPs + Unterstützte GRASPs Eintrittsgebühren + Zutritt + Abonnement + Veröffentlichung + Zahlungen %1$s Zahlungs-URL + Zielgruppe + Richtlinien & Links + Gebühren & Zahlungen Einschränkungen Länder Sprachen Tags + Themen + Alle Länder + Alle Sprachen Veröffentlichungsrichtlinie + Datenschutzerklärung + Allgemeine Geschäftsbedingungen Fehler und Hinweise von diesem Relais Nachrichtenlänge Abonnements Filter Länge der Abonnement-ID Mindestpräfix + Max. Event-Tags Inhaltslänge + Max. Inhaltslänge + Verwirft älter als + Akzeptiert bis zu + %1$s in der Zukunft + vor %1$s + %1$s Nullen + %1$s Bits + Ereignis-Aufbewahrung + Inhaltsgröße + Verbindung + Zugriffskontrolle + Min. PoW-Schwierigkeit Authentifizierung + Authentifizierung erforderlich Zahlung + Zahlung erforderlich + Max. Nachrichtenlänge + Max. Abonnements + Max. Filter pro Abo + Max. Limit (zurückgegebene Ereignisse) + Standardlimit (zurückgegebene Ereignisse) + Max. SubID-Länge Cashu-Token Einlösen An Zap Wallet senden @@ -1187,49 +1231,4 @@ anz der Bedingungen ist erforderlich Empfehlungspaket veröffentlichen Liste löschen Empfehlungspaket löschen - Name (zum @Taggen) - Mein @tag-Name - Relay-Hinweis - Doppelter Beitrag - Fehler - Service-Schlüssel - %1$s wird ausgeführt - %1$s wird ausgeführt (%2$s) - Unterstützte GRASPs - Zutritt - Abonnement - Veröffentlichung - Zahlungen %1$s - Zielgruppe - Richtlinien & Links - Gebühren & Zahlungen - Themen - Alle Länder - Alle Sprachen - Datenschutzerklärung - Allgemeine Geschäftsbedingungen - N/A - Max. Event-Tags - Max. Inhaltslänge - Verwirft älter als - Akzeptiert bis zu - %1$s in der Zukunft - vor %1$s - %1$s Nullen - %1$s Bits - Ereignis-Aufbewahrung - Inhaltsgröße - Verbindung - Zugriffskontrolle - Min. PoW-Schwierigkeit - Authentifizierung erforderlich - Zahlung erforderlich - Max. Nachrichtenlänge - Max. Abonnements - Max. Filter pro Abo - Max. Limit (zurückgegebene Ereignisse) - Standardlimit (zurückgegebene Ereignisse) - Max. SubID-Länge - Standard-Lesezeichen - Deine Standard-Lesezeichen, die viele Clients unterstützen diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 9b9946c55e..8fff23237c 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -15,6 +15,8 @@ सन्देश का अरहस्यीकरण असफल समूह चित्र अभद्र विषयवस्तु + पुनःप्रसारक सूचना + दोहराया गया पत्र कचरालेख इस पुनःप्रसारक से कचरालेख घटनाओं की संख्या पररूपण @@ -121,6 +123,7 @@ पुनःप्रसारक पता प्रकाशित पत्र अष्टक + अपक्रम अपक्रम संयोजन अपक्रमों की संख्या इस सत्र में मुख्य सूचनावली @@ -132,6 +135,8 @@ प्रयोक्ता जोडें पुनःप्रसारक जोडें नाम + नाम (@सूचक के लिए) + मेरा @सूचक नाम प्रदर्शन नाम मेरा प्रदर्शन नाम उष्ट्रपक्षी मक्बढिया @@ -158,6 +163,7 @@ ऐक संदेश का अभिलेखन करें ऐक संदेश का अभिलेखन करें टाँकें तथा दबाए रखें संदेश का अभिलेखन करने के लिए + पुनरभिलेखन ध्वन्यभिलेखन ध्वन्यभिलेखन %1$s आरोहण चल रहा है… @@ -359,6 +365,8 @@ बाधित करें मनुष्यकृत ज्साप विभाजन स्मर्तव्य + मूलविकल्प स्मर्त्तव्य सूची + आपका मूलविकल्प स्मर्त्तव्य सूची जिसका अवलम्बन अनेक ग्राहक करते हैं पाण्डुलिपियाँ निजी स्मर्तव्य सार्वजनिक स्मर्तव्य @@ -642,17 +650,34 @@ अष्टकों में मात्रा जो इस पुनःप्रसारक से प्राप्त हुआ था छलनियाँ तथा घटनाएँ समेत %1$s से पुनःप्रसारक जानकारी प्राप्त करने के प्रयास में अपक्रम हुआ अधिपति + सेवा कुंजी + %1$s चलाया जा रहा है + %1$s (%2$s) चलाया जा रहा है संस्करण क्रमक सम्पर्क अवलम्बन किए हुए निप॰ सूची + अवलम्बित पकड प्रवेश शुल्क + प्रवेशन + ग्राहकता + प्रकाशन + भुगतान %1$s भुगतान जालपता + लक्ष्य दर्शक + नीतियाँ तथा जालयोजक + शुल्क तथा भुगतान परिसीमाएँ देश भाषाएँ विषयसूचक + विषय सूची + सभी देश + सभी भाषाएँ पत्र प्रकाशन नीति + गोपनीयता नीति + नियम तथा अवस्था + अनुपलब्ध अपक्रम तथा सूचनाएँ इस पुनःप्रसारक से संदेश लम्बाई ग्राहकताएँ @@ -661,9 +686,28 @@ न्यूनतम उपसर्ग अधिकतम घटना विषयसूचक विषयवस्तु लम्बाई + अधिकतम विषयवस्तु लम्बाई + इससे पुराना हटाता है + इस तक स्वीकारता है + %1$s भविष्य में + %1$s पूर्व + %1$s शून्यांक + %1$s द्वयंक + घटना रखाई + विषयवस्तु आकार + संयोजकता + अभिगमन नियन्त्रण न्यूनतम श्रमप्रमाण प्रमाणीकरण + प्रमाणीकरण आवश्यक भुगतान + भुगतान आवश्यक + अधिकतम संदेश लम्बाई + अधिकतम ग्राहकताएँ + अधिकतम छलनियाँ प्रति ग्राहकता + अधिकतम सीमा (घटनाएँ प्रतिफलित) + मूलविकल्प सीमा (घटनाएँ प्रतिफलित) + अधिकतम ग्राहकताविभेदक लम्बाई काशयु राशिखण्ड चुकाएँ ज्साप धनकोष को भेजें @@ -685,7 +729,7 @@ ये प्रयोक्ता सूचियाँ हैं जो आप अन्य लोगों के लिए अनुशंसित करते हैं। केवल सार्वजनिक प्रयोक्ता अनुमत। पठितव्य सूचनावली विधियाँ - पण्यक्षेत्र + व्यापारक्षेत्र तत्क्षणप्रसार समुदाय चर्चाएँ diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 05c69d1a0d..d3d01e9930 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -15,6 +15,8 @@ Não foi possível descriptografar a mensagem Imagem do grupo Conteúdo explícito + Aviso do relay + Post duplicado Spam O número de eventos de spam vindo deste relé Representação @@ -121,6 +123,7 @@ Endereço do Relay Postagens Bytes + Erro Erros O número de erros de conexão nesta sessão Feed principal @@ -132,6 +135,8 @@ Adicionar um usuário Adicionar um Relay Nome + Nome (para @marcar) + Meu nome de @tag Nome de Exibição Meu nome de exibição McAwesome Ostrich @@ -358,6 +363,8 @@ Bloquear Divisão manual de Zap Itens Salvos + Marcadores padrão + Seus marcadores padrão que muitos clientes suportam Rascunhos Itens Salvos Privados Itens Salvos Públicos @@ -639,25 +646,61 @@ A quantidade em bytes que foi recebida deste relé, incluindo filtros e eventos Ocorreu um erro ao tentar obter informações do relay de %1$s Proprietário + Chave de serviço + Executando %1$s + Executando %1$s (%2$s) Versão Programa Contato NIPs Suportados + GRASPs compatíveis Taxas de admissão + Entrada + Assinatura + Publicação + Pagamentos %1$s URL de pagamentos + Público-alvo + Políticas & links + Taxas & pagamentos Limitações Países Línguas Cerquilhas + Tópicos + Todos os países + Todos os idiomas Política de postagem + Política de privacidade + Termos & condições Erros e Avisos deste Relé Tamanho da mensagem Assinaturas Filtros Comprimento do ID da assinatura + Prefixo mín. + Máx. tags de evento Tamanho do conteúdo + Máx. tamanho do conteúdo + Descarta mais antigos que + Aceita até + %1$s no futuro + há %1$s + Retenção de eventos + Tamanho do conteúdo + Conectividade + Controle de acesso + Dificuldade mínima de PoW Autenticação + Autenticação obrigatória Pagamento + Pagamento obrigatório + Máx. tamanho da mensagem + Máx. inscrições + Máx. filtros por inscrição + Limite máx. (eventos retornados) + Limite padrão (eventos retornados) + Comprimento máx. do SubID Token Cashu Resgatar Enviar para Zap Wallet @@ -1181,50 +1224,4 @@ Transmitir pacote de recomendações Excluir lista Excluir pacote de recomendações - Nome (para @marcar) - Meu nome de @tag - Aviso do relay - Post duplicado - Erro - Chave de serviço - Executando %1$s - Executando %1$s (%2$s) - GRASPs compatíveis - Entrada - Assinatura - Publicação - Pagamentos %1$s - Público-alvo - Políticas & links - Taxas & pagamentos - Tópicos - Todos os países - Todos os idiomas - Política de privacidade - Termos & condições - N/A - Prefixo mín. - Máx. tags de evento - Máx. tamanho do conteúdo - Descarta mais antigos que - Aceita até - %1$s no futuro - há %1$s - %1$s zeros - %1$s bits - Retenção de eventos - Tamanho do conteúdo - Conectividade - Controle de acesso - Dificuldade mínima de PoW - Autenticação obrigatória - Pagamento obrigatório - Máx. tamanho da mensagem - Máx. inscrições - Máx. filtros por inscrição - Limite máx. (eventos retornados) - Limite padrão (eventos retornados) - Comprimento máx. do SubID - Marcadores padrão - Seus marcadores padrão que muitos clientes suportam diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index f4646c2261..623a0eadb0 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -15,6 +15,8 @@ Kunde inte dekryptera meddelandet Grupp bild Explicit Innehåll + Relay-meddelande + Duplicerat inlägg Spam Antal skräpposthändelser från detta relä Imitation @@ -121,6 +123,7 @@ Relä Adress Inlägg Bytes + Fel Fel Antal anslutningsfel under denna session Hem Flöde @@ -132,6 +135,8 @@ Lägg till en användare Lägg till Relä Namn + Namn (för @taggning) + Mitt @tag-namn Visningsnamn Mitt visningsnamn Struts McAwesome @@ -358,6 +363,8 @@ Blockera Manuell Zap delning Bokmärken + Standardbokmärken + Dina standardbokmärken som många klienter stödjer Utkast Privata Bokmärken Publika Bokmärken @@ -638,25 +645,63 @@ Mängden data i byte som mottogs från detta relä, inklusive filter och händelser Ett fel inträffade vid försök att hämta information från Relay %1$s Ägare + Service-nyckel + Kör %1$s + Kör %1$s (%2$s) Version Programvara Kontakt Stödda NIPs + Stödda GRASPs Entréavgifter + Inträde + Prenumeration + Publicering + Betalningar %1$s Betalnings-URL + Målgrupp + Policyer & länkar + Avgifter & betalningar Begränsningar Länder Språk Taggar + Ämnen + Alla länder + Alla språk Publiceringspolicy + Integritetspolicy + Villkor & bestämmelser Fel och meddelanden från detta relä Meddelandelängd Prenumerationer Filter Längd på prenumerations-ID + Min. prefix + Max. eventtaggar Innehållslängd + Max. innehållslängd + Kastar bort äldre än + Accepterar upp till + %1$s i framtiden + för %1$s sedan + %1$s nollor + %1$s bitar + Lagringstid för händelser + Innehållsstorlek + Anslutning + Åtkomstkontroll + Min. PoW-svårighet Autentisering + Autentisering krävs Betalning + Betalning krävs + Max. meddelandelängd + Max. prenumerationer + Max. filter per prenumeration + Maxgräns (returnerade händelser) + Standardgräns (returnerade händelser) + Max. SubID-längd Cashu Token Inlösen Skicka till Zap Plånbok @@ -1180,50 +1225,4 @@ Sänd rekommendationspaket Ta bort lista Ta bort rekommendationspaket - Namn (för @taggning) - Mitt @tag-namn - Relay-meddelande - Duplicerat inlägg - Fel - Service-nyckel - Kör %1$s - Kör %1$s (%2$s) - Stödda GRASPs - Inträde - Prenumeration - Publicering - Betalningar %1$s - Målgrupp - Policyer & länkar - Avgifter & betalningar - Ämnen - Alla länder - Alla språk - Integritetspolicy - Villkor & bestämmelser - N/A - Min. prefix - Max. eventtaggar - Max. innehållslängd - Kastar bort äldre än - Accepterar upp till - %1$s i framtiden - för %1$s sedan - %1$s nollor - %1$s bitar - Lagringstid för händelser - Innehållsstorlek - Anslutning - Åtkomstkontroll - Min. PoW-svårighet - Autentisering krävs - Betalning krävs - Max. meddelandelängd - Max. prenumerationer - Max. filter per prenumeration - Maxgräns (returnerade händelser) - Standardgräns (returnerade händelser) - Max. SubID-längd - Standardbokmärken - Dina standardbokmärken som många klienter stödjer diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 7141438263..6cbc2e7d59 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -15,6 +15,8 @@ 无法解密消息 群聊图片 明确内容 + 中继通知 + 重复的贴文 垃圾信息 来自该中继的垃圾事件数量 冒充 @@ -121,6 +123,7 @@ 中继器地址 帖文 字节 + 错误 错误 该会话中产生的连接错误数量 主页 @@ -132,6 +135,8 @@ 添加用户 添加中继器 名称 + 名称 (用于 @ 标记时显示) + 我的 @ 名称 显示名称 我的显示名称 Nostr 太酷了 @@ -360,6 +365,8 @@ 仅阻止 手动 Zap 拆分 书签 + 默认书签 + 许多客户端支持默认书签 草稿 私人书签 公开书签 @@ -643,17 +650,34 @@ 从该中继接收事件和过滤响应所使用的数据量 尝试从 %1$s 获取中继器信息时出错 机主 + 服务密钥 + 正在运行 %1$s + 正在运行 %1$s (%2$s) 版本 软件 联络 支持的 NIPs + 支持 Grasps 接纳费 + 接纳 + 订阅 + 发布 + 付款 %1$s 付款网址 + 目标受众 + 策略 & 链接 + 费用 & 付款 限制 国家 语言 标签 + 话题 + 所有国家和地区 + 所有语言 发布政策 + 隐私政策 + 使用条款 + N/A 该中继的错误和通知 消息长度 订阅 @@ -662,9 +686,28 @@ 最少前缀 最多事件标签 内容长度 + 最大内容长度 + 丢弃旧于 + 最多接受 + 未来 %1$s + %1$s 之前 + %1$s 整 + %1$s 位 + 事件保留 + 内容大小 + 连通性 + 访问控制 最低工作量证明 认证 + 需要认证 支付 + 需要付费 + 最大消息长度 + 最大订阅连接数 + 最多订阅过滤器 + 最大数量(返回事件) + 默认限制(返回事件) + 最大 SubID 长度 Cashu 代币 兑换 发送到打闪钱包 From 1c26ccecc0173b755d24512ec7d7efe8b3019416 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 11:15:03 -0500 Subject: [PATCH 03/15] Adds support for NipTexts on Amethyst --- .../amethyst/model/LocalCache.kt | 47 +++++ .../subassemblies/SearchPostsByText.kt | 2 + .../amethyst/ui/note/NoteCompose.kt | 3 + .../amethyst/ui/note/elements/ForkInfo.kt | 4 +- .../amethyst/ui/note/types/Nip.kt | 196 ++++++++++++++++++ .../amethyst/ui/screen/TopNavFilterState.kt | 3 + .../dal/FollowPackFeedNewThreadFeedFilter.kt | 4 +- .../datasource/FilterPostsByHashtags.kt | 2 + .../nip01Core/FilterHomePostsByHashtags.kt | 2 + .../nip65Follows/FilterHomePostsByAuthors.kt | 2 + .../FilterHomePostsFromCommunities.kt | 3 + .../dal/NotificationFeedFilter.kt | 2 + .../datasource/FilterUserProfilePosts.kt | 2 + .../mutual/dal/UserProfileMutualFeedFilter.kt | 2 + .../dal/UserProfileNewThreadFeedFilter.kt | 2 + .../loggedIn/threadview/ThreadFeedView.kt | 23 ++ amethyst/src/main/res/values/strings.xml | 1 + .../experimental/forks/IForkableEvent.kt | 4 + .../experimental/nipsOnNostr/NipTextEvent.kt | 151 ++++++++++++++ .../nipsOnNostr/TagArrayBuilderExt.kt | 29 +++ .../experimental/nipsOnNostr/tags/ForkTag.kt | 145 +++++++++++++ .../tags/TitleTag.kt} | 29 +-- .../quartz/nip10Notes/BaseThreadedEvent.kt | 3 +- .../quartz/nip54Wiki/TagArrayBuilderExt.kt | 13 ++ .../quartz/nip54Wiki/WikiNoteEvent.kt | 4 +- .../quartz/utils/EventFactory.kt | 2 + 26 files changed, 659 insertions(+), 21 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/TagArrayBuilderExt.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/ForkTag.kt rename quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/{forks/BaseThreadedEventExt.kt => nipsOnNostr/tags/TitleTag.kt} (57%) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 1b452063f1..0bad1548b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -46,6 +46,7 @@ import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStory import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent @@ -665,6 +666,51 @@ object LocalCache : ILocalCache { wasVerified: Boolean, ) = consumeRegularEvent(event, relay, wasVerified) + fun consume( + event: NipTextEvent, + relay: NormalizedRelayUrl?, + wasVerified: Boolean, + ): Boolean { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + val isVerified = + if (version.event == null && (wasVerified || justVerify(event))) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + true + } else { + wasVerified + } + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event?.id == event.id) return wasVerified + + if (antiSpam.isSpam(event, relay)) { + return false + } + + if (isVerified || justVerify(event)) { + val replyTo = computeReplyTo(event) + + if (event.createdAt > (note.createdAt() ?: 0L)) { + note.loadEvent(event, author, replyTo) + + refreshNewNoteObservers(note) + + return true + } + } + + return false + } + fun consume( event: LongTextNoteEvent, relay: NormalizedRelayUrl?, @@ -2852,6 +2898,7 @@ object LocalCache : ILocalCache { is MetadataEvent -> consume(event, relay, wasVerified) is MuteListEvent -> consume(event, relay, wasVerified) is NNSEvent -> consume(event, relay, wasVerified) + is NipTextEvent -> consume(event, relay, wasVerified) is OtsEvent -> consume(event, relay, wasVerified) is PictureEvent -> consume(event, relay, wasVerified) is PrivateDmEvent -> consume(event, relay, wasVerified) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt index dd1cf3e629..3c69f4fad0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/searchCommand/subassemblies/SearchPostsByText.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent @@ -81,6 +82,7 @@ val SearchPostsByTextKinds3 = InteractiveStoryPrologueEvent.KIND, InteractiveStorySceneEvent.KIND, FollowListEvent.KIND, + NipTextEvent.KIND, ) fun searchPostsByText( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index f9db3f4225..d707d87fc9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -118,6 +118,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status +import com.vitorpamplona.amethyst.ui.note.types.RenderNipContent import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval @@ -167,6 +168,7 @@ import com.vitorpamplona.quartz.experimental.forks.isAFork import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope @@ -727,6 +729,7 @@ private fun RenderNoteRow( is ReportEvent -> RenderReport(baseNote, quotesLeft, backgroundColor, accountViewModel, nav) is LongTextNoteEvent -> RenderLongFormContent(baseNote, accountViewModel, nav) is WikiNoteEvent -> RenderWikiContent(baseNote, accountViewModel, nav) + is NipTextEvent -> RenderNipContent(baseNote, accountViewModel, nav) is BadgeAwardEvent -> RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav) is FhirResourceEvent -> RenderFhirResource(baseNote, accountViewModel, nav) is PeopleListEvent -> DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index b8fa57650c..6ad3d4bad0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -83,7 +83,7 @@ fun ForkInformationRowLightColor( nav: INav, ) { val noteState by observeNote(originalVersion, accountViewModel) - val note = noteState?.note ?: return + val note = noteState.note val author = note.author ?: return val route = remember(note) { routeFor(note, accountViewModel.account) } @@ -129,7 +129,7 @@ fun ForkInformationRow( nav: INav, ) { val noteState by observeNote(originalVersion, accountViewModel) - val note = noteState?.note ?: return + val note = noteState.note val route = remember(note) { routeFor(note, accountViewModel.account) } if (route != null) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt new file mode 100644 index 0000000000..557e26c468 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.types + +import android.R.attr.label +import android.R.attr.maxLines +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel +import com.vitorpamplona.amethyst.ui.theme.QuoteBorder +import com.vitorpamplona.amethyst.ui.theme.Size5dp +import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.theme.subtleBorder +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds + +@Composable +fun RenderNipContent( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event as? NipTextEvent ?: return + + NipNoteHeader(noteEvent, note, accountViewModel, nav) +} + +@Composable +private fun NipNoteHeader( + noteEvent: NipTextEvent, + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val title = remember(noteEvent) { noteEvent.title() } + val kinds = remember(noteEvent) { noteEvent.kinds() } + + Column( + modifier = + Modifier + .padding(top = Size5dp) + .clip(shape = QuoteBorder) + .border( + 1.dp, + MaterialTheme.colorScheme.subtleBorder, + QuoteBorder, + ), + verticalArrangement = SpacedBy5dp, + ) { + title?.let { + Text( + text = it, + style = MaterialTheme.typography.titleMedium, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, top = 10.dp), + ) + } + Text( + text = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content }, + style = MaterialTheme.typography.bodySmall, + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + if (kinds.isNotEmpty()) { + FlowRow( + modifier = + Modifier + .fillMaxWidth() + .padding(start = 10.dp, end = 10.dp, bottom = 10.dp), + horizontalArrangement = SpacedBy5dp, + verticalArrangement = SpacedBy5dp, + itemVerticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.kinds), + ) + kinds.forEach { + NoPaddingSuggestionChip( + label = it.toString(), + ) + } + } + } + } +} + +@Preview +@Composable +fun NipNoteHeaderPreview() { + val event = + NipTextEvent( + id = "eb2b05394ff0014bb6a79c2eacfd1c80696821592f4dbf86c950c4bf16614aa0", + pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + createdAt = 1767978398, + content = "Trusted Translations\n--------------------\n\n`draft` `optional`\n\nThis NIP allows anyone to post a translation for any event on a `kind:76`.\n\n```js\n{\n \"kind\": 76,\n \"tags\": [\n [\"e\", \"\u003coriginal_event_id\u003e\", \"\u003crelay\u003e\"]\n [\"k\", \"\u003coriginal_event_kind\u003e\"]\n [\"l\", \"\u003ctranslated_to_language_country\u003e\"], // ISO 639-1: \"en\", \"es\", ...\n [\"l\", \"\u003ctranslated_to_language_code\u003e\"], // ISO 639-1: \"en-us\", \"en-br\"\n [\"s\", \"title\", \"translated title tag\"]\n [\"s\", \"summary\", \"translated summary tag\"]\n ],\n \"content\": \"this is a translated version of the original content\",\n // ...other fields\n}\n```\n\n`e` tag points to the event being translated, `k` tag points to the kind of that event.\n\n`l` tags define the language this was translated to in lowercase codes as defined by ISO 639-1. \n\n`s` tags are the translations for tags in the original event. \n\n`.content` contains the translation of the original `.content`\n\nClients SHOULD request translations by `e` and `l` tags spoken by their user. For every tag being rendered, Clients SHOULD look for their translated versions.\n\nProviders SHOULD use the event id in the filter to know which events need translations.\n\nProviders MAY store their translations behind a paid relay with NIP-42 auth.\n\n## Declaring Translation Providers\n\nKind `10041` lists the user's authorized translation providers. Each `p` tag is followed by the `pubkey` of the service publishing kind 76s, and the relay translations can be found. Users can specify these publicly or privately by JSON-stringifying and encrypting the tag list in the `.content` using NIP-44. \n\n```js\n{\n \"kind\": 10041,\n \"tags\": [\n [\"p\", \"4fd5e210530e4f6b2cb083795834bfe5108324f1ed9f00ab73b9e8fcfe5f12fe\", \"wss://translations.nostr.com\"],\n [\"l\", \"\u003ctranslated_to_language_country\u003e\"], // ISO 639-1: \"en\", \"es\", ...\n [\"l\", \"\u003ctranslated_to_language_code\u003e\"], // ISO 639-1: \"en-us\", \"en-br\"\n //...\n}\n```\n\n`l` tags in this event are the languages the user understands and wants translations to.\n\nProviders SHOULD create the `10041` event and post to the user's outbox relay.", + sig = "7fa9f1d49c41c7bfbdad6d089d1c6685777c86de8fbda7662a630888658839f0444cb1398385f759461c09191b287fa222eec0ffe22b3fa8cf740f71aab9dc21", + tags = + arrayOf( + arrayOf("d", "trusted-translations"), + arrayOf("title", "Trusted Translations"), + arrayOf("k", "1011"), + arrayOf("k", "10041"), + arrayOf("k", "1011"), + arrayOf("k", "10041"), + arrayOf("k", "1011"), + arrayOf("k", "10041"), + arrayOf("k", "1011"), + arrayOf("k", "10041"), + arrayOf("client", "nostrhub.io"), + ), + ) + + LocalCache.justConsume(event, null, true) + val note = LocalCache.getOrCreateNote(event.id) + + ThemeComparisonColumn( + toPreview = { + NipNoteHeader( + noteEvent = event, + note = note, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NoPaddingSuggestionChip( + label: String, + modifier: Modifier = Modifier, +) { + Surface( + shape = MaterialTheme.shapes.extraSmall, // Use a small shape for chip look + color = MaterialTheme.colorScheme.secondaryContainer, // Default chip color + modifier = modifier, + ) { + Text( + text = label, + // Apply desired internal padding to the Text itself + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt index 6cf7aea7da..9ee0b06328 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/TopNavFilterState.kt @@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -392,6 +393,7 @@ val DEFAULT_FEED_KINDS = LiveActivitiesChatMessageEvent.KIND, LiveActivitiesEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, InteractiveStoryPrologueEvent.KIND, ) @@ -406,6 +408,7 @@ val DEFAULT_COMMUNITY_FEEDS = AudioTrackEvent.KIND, PinListEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, CommunityPostApprovalEvent.KIND, InteractiveStoryPrologueEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt index ac8b756a21..ca20492aa4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/followPacks/feed/dal/FollowPackFeedNewThreadFeedFilter.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -49,12 +50,13 @@ class FollowPackFeedNewThreadFeedFilter( val followPackNote: AddressableNote, val account: Account, ) : AdditiveFeedFilter() { - companion object Companion { + companion object { val ADDRESSABLE_KINDS = listOf( AudioTrackEvent.KIND, InteractiveStoryPrologueEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, ClassifiedsEvent.KIND, LongTextNoteEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt index 5b0c37659b..3071625700 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/hashtag/datasource/FilterPostsByHashtags.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Commen import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStorySceneEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter @@ -58,6 +59,7 @@ val PostsByHashtagKinds2 = InteractiveStorySceneEvent.KIND, AudioTrackEvent.KIND, AudioHeaderEvent.KIND, + NipTextEvent.KIND, ) fun filterPostsByHashtags( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt index 8e9eddd2be..b7c3ef37c4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip01Core/FilterHomePostsByHashtags.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip22Comments.filterHomePostsByScopes import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -52,6 +53,7 @@ val HomePostsBuHashtagsKinds = InteractiveStoryPrologueEvent.KIND, CommentEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, VoiceEvent.KIND, VoiceReplyEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt index 46bebbc444..5ac4955e90 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip65Follows/FilterHomePostsByAuthors.kt @@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthors import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -53,6 +54,7 @@ val HomePostsNewThreadKinds = LongTextNoteEvent.KIND, HighlightEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, PollNoteEvent.KIND, InteractiveStoryPrologueEvent.KIND, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt index a914febf50..b96effdd4a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/datasource/nip72Communities/FilterHomePostsFromCommunities.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.nip72Commu import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl @@ -41,6 +42,7 @@ val HomePostsFromCommunityKinds = ClassifiedsEvent.KIND, HighlightEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, CommunityPostApprovalEvent.KIND, CommentEvent.KIND, InteractiveStoryPrologueEvent.KIND, @@ -53,6 +55,7 @@ val HomePostsFromCommunityKindsStr = ClassifiedsEvent.KIND.toString(), HighlightEvent.KIND.toString(), WikiNoteEvent.KIND.toString(), + NipTextEvent.KIND.toString(), CommunityPostApprovalEvent.KIND.toString(), CommentEvent.KIND.toString(), InteractiveStoryPrologueEvent.KIND.toString(), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index fac9b7661b..d07a6f2847 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.forks.forkFromVersion import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser @@ -77,6 +78,7 @@ class NotificationFeedFilter( listOf( AudioTrackEvent.KIND, WikiNoteEvent.KIND, + NipTextEvent.KIND, ClassifiedsEvent.KIND, LongTextNoteEvent.KIND, CalendarTimeSlotEvent.KIND, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt index dbbd86c0e4..991ed0f800 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfilePosts.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter @@ -58,6 +59,7 @@ val UserProfilePostKinds2 = listOf( TorrentEvent.KIND, TorrentCommentEvent.KIND, + NipTextEvent.KIND, InteractiveStoryPrologueEvent.KIND, CommentEvent.KIND, VoiceReplyEvent.KIND, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt index 22a90a03ef..dbb4981bff 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/dal/UserProfileMutualFeedFilter.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent @@ -76,6 +77,7 @@ class UserProfileMutualFeedFilter( it.event is GenericRepostEvent || it.event is LongTextNoteEvent || it.event is WikiNoteEvent || + it.event is NipTextEvent || it.event is PollNoteEvent || it.event is HighlightEvent || it.event is InteractiveStoryPrologueEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt index 3b4b1b7277..4aadd02778 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/dal/UserProfileNewThreadFeedFilter.kt @@ -30,6 +30,7 @@ import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -77,6 +78,7 @@ class UserProfileNewThreadFeedFilter( it.event is GenericRepostEvent || it.event is LongTextNoteEvent || it.event is WikiNoteEvent || + it.event is NipTextEvent || it.event is PollNoteEvent || it.event is HighlightEvent || it.event is InteractiveStoryPrologueEvent || diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index e3f5068b9b..b48551417b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -191,6 +191,7 @@ import com.vitorpamplona.quartz.experimental.forks.forkFromAddress import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -545,6 +546,7 @@ private fun FullBleedNoteCompose( is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) is LongTextNoteEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel) is WikiNoteEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav) + is NipTextEvent -> RenderNipHeaderForThread(noteEvent, accountViewModel, nav) is ClassifiedsEvent -> RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav) } @@ -1157,3 +1159,24 @@ private fun RenderWikiHeaderForThread( } } } + +@Composable +private fun RenderNipHeaderForThread( + noteEvent: NipTextEvent, + accountViewModel: AccountViewModel, + nav: INav, +) { + val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } + + Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { + Column { + forkedAddress?.let { + LoadAddressableNote(it, accountViewModel) { originalVersion -> + if (originalVersion != null) { + ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) + } + } + } + } + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index d033e2736d..e9b34537c2 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1465,4 +1465,5 @@ Delete List Delete Pack + Kinds diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt new file mode 100644 index 0000000000..47fddb817e --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt @@ -0,0 +1,4 @@ +package com.vitorpamplona.quartz.experimental.forks + +class IForkableEvent { +} \ No newline at end of file diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt new file mode 100644 index 0000000000..f00e84c3a7 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nipsOnNostr + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.forks.parseFork +import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate +import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag +import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag +import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds +import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag +import com.vitorpamplona.quartz.nip19Bech32.addressHints +import com.vitorpamplona.quartz.nip19Bech32.addressIds +import com.vitorpamplona.quartz.nip19Bech32.eventHints +import com.vitorpamplona.quartz.nip19Bech32.eventIds +import com.vitorpamplona.quartz.nip22Comments.RootScope +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt +import com.vitorpamplona.quartz.nip50Search.SearchableEvent +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid + +@Immutable +class NipTextEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), + AddressableEvent, + RootScope, + EventHintProvider, + AddressHintProvider, + SearchableEvent { + override fun indexableContent() = "title: " + title() + "\n" + content + + override fun dTag() = tags.dTag() + + override fun aTag(relayHint: NormalizedRelayUrl?) = ATag(kind, pubKey, dTag(), relayHint) + + override fun address() = Address(kind, pubKey, dTag()) + + override fun addressTag() = Address.assemble(kind, pubKey, dTag()) + + override fun eventHints(): List { + val qHints = tags.mapNotNull(QTag::parseEventAsHint) + val nip19Hints = citedNIP19().eventHints() + + return qHints + nip19Hints + } + + override fun linkedEventIds(): List { + val qHints = tags.mapNotNull(QTag::parseEventId) + val nip19Hints = citedNIP19().eventIds() + + return qHints + nip19Hints + } + + override fun addressHints(): List { + val aHints = tags.mapNotNull(ATag::parseAsHint) + val qHints = tags.mapNotNull(QTag::parseAddressAsHint) + val nip19Hints = citedNIP19().addressHints() + + return aHints + qHints + nip19Hints + } + + override fun linkedAddressIds(): List { + val aHints = tags.mapNotNull(ATag::parseAddressId) + val qHints = tags.mapNotNull(QTag::parseAddressId) + val nip19Hints = citedNIP19().addressIds() + + return aHints + qHints + nip19Hints + } + + fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + + fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress) + + fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork) + + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) + + fun summary() = + content.lineSequence().drop(1).firstNotNullOfOrNull { + if (it.isBlank() || + it.startsWith("---") || + it.startsWith("`draft") || + it.startsWith("#") + ) { + null + } else { + it + } + } + + companion object { + const val KIND = 30817 + const val KIND_STR = "30817" + + @OptIn(ExperimentalUuidApi::class) + fun build( + description: String, + title: String, + kinds: List, + dTag: String = Uuid.random().toString(), + createdAt: Long = TimeUtils.now(), + initializer: TagArrayBuilder.() -> Unit = {}, + ) = eventTemplate(KIND, description, createdAt) { + dTag(dTag) + alt("NIP: $title") + + title(title) + kinds(kinds) + + initializer() + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..f77989cfad --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/TagArrayBuilderExt.kt @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nipsOnNostr + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip01Core.tags.kinds.KindTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.kinds(kinds: List) = addAll(KindTag.assemble(kinds)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/ForkTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/ForkTag.kt new file mode 100644 index 0000000000..27f9547fb7 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/ForkTag.kt @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.experimental.nipsOnNostr.tags + +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer +import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent +import com.vitorpamplona.quartz.utils.arrayOfNotNull +import com.vitorpamplona.quartz.utils.ensure + +class ForkTag( + val address: Address, + val relayHint: NormalizedRelayUrl? = null, +) { + fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) + + fun toTagArray() = assemble(address, relayHint) + + fun toTagIdOnly() = assemble(address, null) + + companion object { + const val TAG_NAME = "a" + + fun isTagged(tag: Array) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], NipTextEvent.KIND_STR) + + fun isTagged( + tag: Array, + addressId: String, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == addressId + + fun isTagged( + tag: Array, + address: ForkTag, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag() + + fun isIn( + tag: Array, + addressIds: Set, + ) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds + + fun parse(tag: Array): ForkTag? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure( + Address.Companion.isOfKind( + tag[1], + CommunityDefinitionEvent.Companion.KIND_STR, + ), + ) { return null } + + val address = Address.Companion.parse(tag[1]) ?: return null + val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.Companion.normalizeOrNull(it) } + return ForkTag(address, relayHint) + } + + fun parseValidAddress(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure( + Address.Companion.isOfKind( + tag[1], + CommunityDefinitionEvent.Companion.KIND_STR, + ), + ) { return null } + return Address.Companion.parse(tag[1])?.toValue() + } + + fun parseAddress(tag: Array): Address? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + val address = Address.parse(tag[1]) ?: return null + ensure(address.kind == NipTextEvent.KIND) { return null } + return address + } + + fun parseAddressId(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure( + Address.isOfKind( + tag[1], + NipTextEvent.KIND_STR, + ), + ) { return null } + return tag[1] + } + + fun parseAsHint(tag: Array): AddressHint? { + ensure(tag.has(2)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure( + Address.isOfKind( + tag[1], + NipTextEvent.KIND_STR, + ), + ) { return null } + ensure(tag[2].isNotEmpty()) { return null } + + val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) + ensure(relayHint != null) { return null } + + return AddressHint(tag[1], relayHint) + } + + fun assemble( + aTagId: String, + relay: NormalizedRelayUrl?, + ) = arrayOfNotNull(TAG_NAME, aTagId, relay?.url, "fork") + + fun assemble( + address: Address, + relay: NormalizedRelayUrl?, + ) = assemble(address.toValue(), relay) + + fun assemble( + kind: Int, + pubKey: String, + dTag: String, + relay: NormalizedRelayUrl?, + ) = assemble(Address.assemble(kind, pubKey, dTag), relay) + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/TitleTag.kt similarity index 57% rename from quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt rename to quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/TitleTag.kt index 0d38a4f5bc..aa57f067d2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/BaseThreadedEventExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/tags/TitleTag.kt @@ -18,21 +18,22 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.quartz.experimental.forks +package com.vitorpamplona.quartz.experimental.nipsOnNostr.tags -import com.vitorpamplona.quartz.nip01Core.core.Address -import com.vitorpamplona.quartz.nip01Core.core.HexKey -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent -import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag +import com.vitorpamplona.quartz.nip01Core.core.has +import com.vitorpamplona.quartz.utils.ensure -fun BaseThreadedEvent.isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } +class TitleTag { + companion object { + const val TAG_NAME = "title" -fun BaseThreadedEvent.forkFromAddress() = - tags.firstOrNull { it.size > 3 && it[0] == "a" && it[3] == "fork" }?.let { - val aTagValue = it[1] - Address.parse(aTagValue) + fun parse(tag: Array): String? { + ensure(tag.has(1)) { return null } + ensure(tag[0] == TAG_NAME) { return null } + ensure(tag[1].isNotEmpty()) { return null } + return tag[1] + } + + fun assemble(title: String) = arrayOf(TAG_NAME, title) } - -fun BaseThreadedEvent.forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork) - -fun BaseThreadedEvent.isForkFromAddressWithPubkey(authorHex: HexKey) = tags.any { it.size > 3 && it[0] == "a" && it[3] == "fork" && it[1].contains(authorHex) } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt index e58c8d1ce5..b9fb1a08c2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/BaseThreadedEvent.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedATags import com.vitorpamplona.quartz.nip01Core.tags.people.taggedUsers @@ -79,7 +80,7 @@ open class BaseThreadedEvent( val tagAddresses = taggedATags() .filter { aTag -> - aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || aTag.kind != WikiNoteEvent.KIND) + aTag.kind != CommunityDefinitionEvent.KIND && (kind != WikiNoteEvent.KIND || aTag.kind != WikiNoteEvent.KIND) && (kind != NipTextEvent.KIND || aTag.kind != NipTextEvent.KIND) // removes forks from itself. }.map { it.toTag() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt new file mode 100644 index 0000000000..f460e981e7 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt @@ -0,0 +1,13 @@ +package com.vitorpamplona.quartz.nip54Wiki + +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag + +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) + +fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) + +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) + +fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) \ No newline at end of file diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index ebc6d3a160..e2908f2b4c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -131,9 +131,7 @@ class WikiNoteEvent( fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) override fun publishedAt(): Long? { - val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse) - - if (publishedAt == null) return null + val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse) ?: return null // removes posts in the future. return if (publishedAt <= createdAt) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt index ef11c9d3fc..2d6162696c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/EventFactory.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.data.FileStorageEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent import com.vitorpamplona.quartz.experimental.nipA3.PaymentTargetsEvent +import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.nns.NNSEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent @@ -266,6 +267,7 @@ class EventFactory { MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig) NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig) + NipTextEvent.KIND -> NipTextEvent(id, pubKey, createdAt, tags, content, sig) NostrConnectEvent.KIND -> NostrConnectEvent(id, pubKey, createdAt, tags, content, sig) NIP90StatusEvent.KIND -> NIP90StatusEvent(id, pubKey, createdAt, tags, content, sig) NIP90ContentDiscoveryRequestEvent.KIND -> NIP90ContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) From 1ead2983ac52d253b1d204a17bd4f3c3e6d94dff Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 11:36:11 -0500 Subject: [PATCH 04/15] Moves to a fork model via interfaces to start creating specific screens for each event type --- .../amethyst/model/LocalCache.kt | 1 - .../amethyst/ui/note/NoteCompose.kt | 5 +- .../amethyst/ui/note/ReactionsRow.kt | 25 ++++--- .../amethyst/ui/note/elements/ForkInfo.kt | 8 +-- .../dal/NotificationFeedFilter.kt | 27 +++++--- .../experimental/forks/IForkableEvent.kt | 34 +++++++++- .../experimental/forks/MarkedETagExt.kt | 8 +++ .../experimental/nipsOnNostr/NipTextEvent.kt | 10 +-- .../quartz/nip10Notes/TextNoteEvent.kt | 10 +++ .../quartz/nip54Wiki/TagArrayBuilderExt.kt | 32 +++++++-- .../quartz/nip54Wiki/WikiNoteEvent.kt | 68 +++++++++++++------ 11 files changed, 169 insertions(+), 59 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 0bad1548b6..ad2e47c4e6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -805,7 +805,6 @@ object LocalCache : ILocalCache { fun computeReplyTo(event: Event): List = when (event) { is PollNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } - is WikiNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is LongTextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } is GitReplyEvent -> event.tagsWithoutCitations().filter { it != event.repository()?.toTag() }.mapNotNull { checkGetOrCreateNote(it) } is TextNoteEvent -> event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index d707d87fc9..7956f2a8de 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -164,7 +164,7 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.forks.isAFork +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent @@ -173,7 +173,6 @@ import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.tags.geohash.geoHashOrScope import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip13Pow.strongPoWOrNull import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent @@ -1088,7 +1087,7 @@ fun SecondUserInfoRow( modifier = UserNameMaxRowHeight, ) { Column(modifier = remember(noteEvent) { Modifier.weight(1f) }) { - if (noteEvent is BaseThreadedEvent && noteEvent.isAFork()) { + if (noteEvent is IForkableEvent && noteEvent.isAFork()) { ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav) } else { ObserveDisplayNip05Status(noteAuthor, accountViewModel, nav) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 279a3414f3..a2814a4643 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -159,6 +159,7 @@ import com.vitorpamplona.amethyst.ui.theme.reactionBox import com.vitorpamplona.amethyst.ui.theme.ripple24dp import com.vitorpamplona.amethyst.ui.theme.selectedReactionBoxModifier import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip30CustomEmoji.CustomEmoji import com.vitorpamplona.quartz.nip57Zaps.zapraiser.zapraiserAmount @@ -1394,16 +1395,20 @@ private fun BoostTypeChoicePopup( Text(stringRes(R.string.quote), color = Color.White, textAlign = TextAlign.Center) } - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = onFork, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - ) { - Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center) + // removes the option to fork for now because we do not have screens for + // LongForm, Wiki and NIP posting. + if (baseNote.event is TextNoteEvent) { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = onFork, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(stringRes(R.string.fork), color = Color.White, textAlign = TextAlign.Center) + } } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 6ad3d4bad0..2c52fdaa7d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -47,13 +47,11 @@ import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.nip05 -import com.vitorpamplona.quartz.experimental.forks.forkFromAddress -import com.vitorpamplona.quartz.experimental.forks.forkFromVersion -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent @Composable fun ShowForkInformation( - noteEvent: BaseThreadedEvent, + noteEvent: IForkableEvent, modifier: Modifier, accountViewModel: AccountViewModel, nav: INav, @@ -67,7 +65,7 @@ fun ShowForkInformation( } } } else if (forkedEvent != null) { - LoadNote(forkedEvent.eventId, accountViewModel) { event -> + LoadNote(forkedEvent, accountViewModel) { event -> if (event != null) { ForkInformationRowLightColor(event, modifier, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt index d07a6f2847..b8a5ba010d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/dal/NotificationFeedFilter.kt @@ -29,14 +29,13 @@ import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.amethyst.ui.dal.FilterByListParams import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent -import com.vitorpamplona.quartz.experimental.forks.forkFromVersion -import com.vitorpamplona.quartz.experimental.forks.isForkFromAddressWithPubkey +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -187,7 +186,7 @@ class NotificationFeedFilter( return true } - if (event is BaseThreadedEvent) { + if (event is BaseNoteEvent) { if (note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) { return true } @@ -210,11 +209,23 @@ class NotificationFeedFilter( } val isAuthoredPostCited = event.findCitations().any { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } - val isAuthorDirectlyCited = event.citedUsers().contains(authorHex) - val isAuthorOfAFork = - event.isForkFromAddressWithPubkey(authorHex) || (event.forkFromVersion()?.let { LocalCache.getNoteIfExists(it.eventId)?.author?.pubkeyHex == authorHex } == true) - return isAuthoredPostCited || isAuthorDirectlyCited || isAuthorOfAFork + if (isAuthoredPostCited) return true + + val isAuthorDirectlyCited = event.citedUsers().contains(authorHex) + + if (isAuthorDirectlyCited) return true + + return if (event is IForkableEvent && event.isAFork()) { + val address = event.forkFromAddress() + val version = event.forkFromVersion() + + // Displays notifications about forks + address?.pubKeyHex == authorHex || + (version?.let { LocalCache.getNoteIfExists(it)?.author?.pubkeyHex == authorHex } == true) + } else { + false + } } if (event is ReactionEvent) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt index 47fddb817e..e614cf8667 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/IForkableEvent.kt @@ -1,4 +1,34 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ package com.vitorpamplona.quartz.experimental.forks -class IForkableEvent { -} \ No newline at end of file +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.HexKey + +@Stable +interface IForkableEvent { + fun isAFork(): Boolean + + fun forkFromAddress(): Address? + + fun forkFromVersion(): HexKey? +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt index cc2971896f..7144047771 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/forks/MarkedETagExt.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.quartz.experimental.forks +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag.MARKER @@ -37,3 +38,10 @@ fun MarkedETag.Companion.parseFork(tag: Array): MarkedETag? { ), ) } + +fun MarkedETag.Companion.parseForkedEventId(tag: Array): HexKey? { + if (tag.size < 4 || tag[0] != "e") return null + if (tag[ORDER_MARKER] != MARKER.FORK.code) return null + // ["e", id hex, relay hint, marker, pubkey] + return tag[ORDER_EVT_ID] +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt index f00e84c3a7..07581a7832 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/nipsOnNostr/NipTextEvent.kt @@ -21,7 +21,8 @@ package com.vitorpamplona.quartz.experimental.nipsOnNostr import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.experimental.forks.parseFork +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent +import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent @@ -64,6 +65,7 @@ class NipTextEvent( RootScope, EventHintProvider, AddressHintProvider, + IForkableEvent, SearchableEvent { override fun indexableContent() = "title: " + title() + "\n" + content @@ -105,11 +107,11 @@ class NipTextEvent( return aHints + qHints + nip19Hints } - fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } - fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress) + override fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress) - fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseFork) + override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId) fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt index f50a5dd489..b8ac14da8b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.quartz.nip10Notes import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent +import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider @@ -47,6 +49,7 @@ import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils +import kotlinx.serialization.json.JsonNull.content @Immutable class TextNoteEvent( @@ -60,6 +63,7 @@ class TextNoteEvent( EventHintProvider, AddressHintProvider, PubKeyHintProvider, + IForkableEvent, SearchableEvent { override fun indexableContent() = "Subject: " + subject() + "\n" + content @@ -109,6 +113,12 @@ class TextNoteEvent( return pHints + nip19Hints } + override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + + override fun forkFromAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress) + + override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId) + companion object { const val KIND = 1 const val ALT = "A short note: " diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt index f460e981e7..f6715d8fd2 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/TagArrayBuilderExt.kt @@ -1,13 +1,35 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ package com.vitorpamplona.quartz.nip54Wiki import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder -import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag -fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) +fun TagArrayBuilder.title(title: String) = addUnique(TitleTag.assemble(title)) -fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) +fun TagArrayBuilder.summary(summary: String) = addUnique(SummaryTag.assemble(summary)) -fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) +fun TagArrayBuilder.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl)) -fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) \ No newline at end of file +fun TagArrayBuilder.publishedAt(publishedAt: Long) = addUnique(PublishedAtTag.assemble(publishedAt)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index e2908f2b4c..e81c56fbed 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -21,9 +21,13 @@ package com.vitorpamplona.quartz.nip54Wiki import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent +import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId +import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider @@ -31,13 +35,14 @@ import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl -import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate +import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.publishedAt.PublishedAtProvider -import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent +import com.vitorpamplona.quartz.nip10Notes.BaseNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag import com.vitorpamplona.quartz.nip19Bech32.addressHints @@ -46,10 +51,16 @@ import com.vitorpamplona.quartz.nip19Bech32.eventHints import com.vitorpamplona.quartz.nip19Bech32.eventIds import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys +import com.vitorpamplona.quartz.nip22Comments.RootScope +import com.vitorpamplona.quartz.nip23LongContent.tags.ImageTag import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag -import com.vitorpamplona.quartz.nip31Alts.AltTag +import com.vitorpamplona.quartz.nip23LongContent.tags.SummaryTag +import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag +import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid @Immutable class WikiNoteEvent( @@ -59,12 +70,14 @@ class WikiNoteEvent( tags: Array>, content: String, sig: HexKey, -) : BaseThreadedEvent(id, pubKey, createdAt, KIND, tags, content, sig), +) : BaseNoteEvent(id, pubKey, createdAt, KIND, tags, content, sig), AddressableEvent, EventHintProvider, AddressHintProvider, PubKeyHintProvider, PublishedAtProvider, + IForkableEvent, + RootScope, SearchableEvent { override fun indexableContent() = "title: " + title() + "\nsummary: " + summary() + "\n" + content @@ -124,11 +137,17 @@ class WikiNoteEvent( fun topics() = hashtags() - fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1) + fun title() = tags.firstNotNullOfOrNull(TitleTag::parse) - fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1) + fun image() = tags.firstNotNullOfOrNull(ImageTag::parse) - fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1) + fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse) + + override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" } + + override fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress) + + override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId) override fun publishedAt(): Long? { val publishedAt = tags.firstNotNullOfOrNull(PublishedAtTag::parse) ?: return null @@ -144,20 +163,27 @@ class WikiNoteEvent( companion object { const val KIND = 30818 - suspend fun create( - msg: String, - title: String?, - replyTos: List?, - mentions: List?, - signer: NostrSigner, + @OptIn(ExperimentalUuidApi::class) + fun build( + description: String, + title: String, + summary: String? = null, + image: String? = null, + publishedAt: Long? = null, + dTag: String = Uuid.random().toString(), createdAt: Long = TimeUtils.now(), - ): WikiNoteEvent { - val tags = mutableListOf>() - replyTos?.forEach { tags.add(arrayOf("e", it)) } - mentions?.forEach { tags.add(arrayOf("p", it)) } - title?.let { tags.add(arrayOf("title", it)) } - tags.add(AltTag.assemble("Wiki Post: $title")) - return signer.sign(createdAt, KIND, tags.toTypedArray(), msg) - } + initializer: TagArrayBuilder.() -> Unit = {}, + ): EventTemplate = + eventTemplate(KIND, description, createdAt) { + dTag(dTag) + alt("Wiki entry: $title") + + title(title) + summary?.let { summary(it) } + image?.let { image(it) } + publishedAt?.let { publishedAt(it) } + + initializer() + } } } From 3c12be354e4bead5a5b6438e49cec5e02ade4414 Mon Sep 17 00:00:00 2001 From: davotoula Date: Fri, 9 Jan 2026 11:51:42 +0100 Subject: [PATCH 05/15] change from "hold to record" to "click to start, click to stop" --- .../ui/actions/uploads/RecordAudio.kt | 69 ++++---- .../amethyst/ui/components/ClickableBox.kt | 150 ++++-------------- 2 files changed, 70 insertions(+), 149 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt index c5083fb12c..32af08e0b3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt @@ -37,7 +37,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.ui.components.ClickAndHoldBoxComposable +import com.vitorpamplona.amethyst.ui.components.ToggleableBox import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.delay import kotlinx.coroutines.isActive @@ -52,15 +52,16 @@ fun RecordAudioBox( val mediaRecorder = remember { mutableStateOf(null) } val context = LocalContext.current var elapsedSeconds by remember { mutableIntStateOf(0) } - var wantsToRecord by remember { mutableStateOf(false) } + var pendingPermissionStart by remember { mutableStateOf(false) } // Must be called at Composable scope, not in callback val recordPermissionState = rememberPermissionState(Manifest.permission.RECORD_AUDIO) val scope = rememberCoroutineScope() + val isRecording = mediaRecorder.value != null + DisposableEffect(Unit) { onDispose { - wantsToRecord = false mediaRecorder.value?.stop() mediaRecorder.value = null } @@ -74,8 +75,25 @@ fun RecordAudioBox( } } - LaunchedEffect(recordPermissionState.status.isGranted, wantsToRecord) { - if (recordPermissionState.status.isGranted && wantsToRecord) { + fun stopRecording() { + val result = mediaRecorder.value?.stop() + mediaRecorder.value = null + if (result != null) { + onRecordTaken(result) + } else { + Toast + .makeText( + context, + stringRes(context, R.string.record_a_message_description), + Toast.LENGTH_SHORT, + ).show() + } + } + + // Start recording after permission is granted + LaunchedEffect(recordPermissionState.status.isGranted) { + if (recordPermissionState.status.isGranted && pendingPermissionStart) { + pendingPermissionStart = false startRecording() } } @@ -96,38 +114,21 @@ fun RecordAudioBox( } } - ClickAndHoldBoxComposable( + ToggleableBox( modifier = modifier, - onPress = { - wantsToRecord = true - if (!recordPermissionState.status.isGranted) { - recordPermissionState.launchPermissionRequest() + isActive = isRecording, + onClick = { + if (isRecording) { + stopRecording() } else { - // Start immediately for responsive UX when permission already granted - startRecording() + if (!recordPermissionState.status.isGranted) { + pendingPermissionStart = true + recordPermissionState.launchPermissionRequest() + } else { + startRecording() + } } }, - onRelease = { - wantsToRecord = false - val result = mediaRecorder.value?.stop() - mediaRecorder.value = null - if (result != null) { - onRecordTaken(result) - } else { - // less disruptive than error messages - Toast - .makeText( - context, - stringRes(context, R.string.record_a_message_description), - Toast.LENGTH_SHORT, - ).show() - } - }, - onCancel = { - wantsToRecord = false - mediaRecorder.value?.stop() - mediaRecorder.value = null - }, - content = @Composable { isRecording -> content(isRecording, elapsedSeconds) }, + content = { active -> content(active, elapsedSeconds) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt index 6df79b5cda..a63656d4bc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableBox.kt @@ -28,17 +28,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.PressInteraction -import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale @@ -64,116 +59,6 @@ fun ClickableBox( } } -@Composable -fun ClickAndHoldBox( - modifier: Modifier = Modifier, - onPress: () -> Unit, - onRelease: () -> Unit, - content: @Composable (Boolean) -> Unit, -) { - val interactionSource = remember { MutableInteractionSource() } - val isPressed by interactionSource.collectIsPressedAsState() - - LaunchedEffect(isPressed) { - if (isPressed) { - // Button is pressed - onPress() - } else { - // Button is released - onRelease() - } - } - - // Animation for the button scale - val scale by animateFloatAsState( - targetValue = if (isPressed) 1.5f else 1.0f, // Scale up when recording - animationSpec = tween(durationMillis = 150), // Smooth animation - ) - - // Animation for the button color - val backgroundColor by animateColorAsState( - targetValue = if (isPressed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background, - animationSpec = tween(durationMillis = 150), - ) - - Box( - modifier - .scale(scale) - .background(backgroundColor, CircleShape) - .clickable( - role = Role.Button, - interactionSource = interactionSource, - indication = ripple24dp, - onClick = { }, - ), - contentAlignment = Alignment.Center, - ) { - content(isPressed) - } -} - -@Composable -fun ClickAndHoldBoxComposable( - modifier: Modifier = Modifier, - onPress: () -> Unit, - onRelease: suspend () -> Unit, - onCancel: suspend () -> Unit, - content: @Composable (Boolean) -> Unit, -) { - val interactionSource = remember { MutableInteractionSource() } - var isPressed by remember { mutableStateOf(false) } - - LaunchedEffect(interactionSource) { - val pressInteractions = mutableListOf() - interactionSource.interactions.collect { interaction -> - when (interaction) { - is PressInteraction.Press -> { - if (pressInteractions.isEmpty()) { - onPress() - } - pressInteractions.add(interaction) - } - is PressInteraction.Release -> { - onRelease() - pressInteractions.remove(interaction.press) - } - is PressInteraction.Cancel -> { - onCancel() - pressInteractions.remove(interaction.press) - } - } - isPressed = pressInteractions.isNotEmpty() - } - } - - // Animation for the button scale - val scale by animateFloatAsState( - targetValue = if (isPressed) 1.5f else 1.0f, // Scale up when recording - animationSpec = tween(durationMillis = 150), // Smooth animation - ) - - // Animation for the button color - val backgroundColor by animateColorAsState( - targetValue = if (isPressed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background, - animationSpec = tween(durationMillis = 150), - ) - - Box( - modifier - .scale(scale) - .background(backgroundColor, CircleShape) - .clickable( - role = Role.Button, - interactionSource = interactionSource, - indication = ripple24dp, - onClick = { }, - ), - contentAlignment = Alignment.Center, - ) { - content(isPressed) - } -} - @OptIn(ExperimentalFoundationApi::class) @Composable fun ClickableBox( @@ -195,3 +80,38 @@ fun ClickableBox( content() } } + +@Composable +fun ToggleableBox( + modifier: Modifier = Modifier, + isActive: Boolean, + onClick: () -> Unit, + content: @Composable (Boolean) -> Unit, +) { + // Animation for the button scale + val scale by animateFloatAsState( + targetValue = if (isActive) 1.5f else 1.0f, + animationSpec = tween(durationMillis = 150), + ) + + // Animation for the button color + val backgroundColor by animateColorAsState( + targetValue = if (isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.background, + animationSpec = tween(durationMillis = 150), + ) + + Box( + modifier + .scale(scale) + .background(backgroundColor, CircleShape) + .clickable( + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple24dp, + onClick = onClick, + ), + contentAlignment = Alignment.Center, + ) { + content(isActive) + } +} From 524180bec9da4a4692476beb2f7c9e47367b2092 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 16:41:22 +0100 Subject: [PATCH 06/15] change icon to stop icon --- .../amethyst/ui/actions/uploads/RecordingIndicators.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt index a4e3ecd650..0f7fb09469 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordingIndicators.kt @@ -35,7 +35,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.FiberManualRecord +import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -206,8 +206,8 @@ fun FloatingRecordingIndicator( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = innerPadding), ) { - // Pulsing red dot - val infiniteTransition = rememberInfiniteTransition(label = "recording_dot") + // Pulsing stop square + val infiniteTransition = rememberInfiniteTransition(label = "recording_stop") val dotAlpha by infiniteTransition.animateFloat( initialValue = 1f, targetValue = 0.5f, @@ -220,7 +220,7 @@ fun FloatingRecordingIndicator( ) Icon( - imageVector = Icons.Default.FiberManualRecord, + imageVector = Icons.Default.Stop, contentDescription = recordingLabel, tint = Color.White, modifier = From 5faa439a8b9798051b1499f102987453a479a76c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 16:55:06 +0100 Subject: [PATCH 07/15] fix re-record button --- .../screen/loggedIn/home/VoiceReplyScreen.kt | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt index da1dd4b1cc..15f30bb19a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt @@ -35,6 +35,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -53,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview +import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.NoteCompose @@ -212,29 +214,44 @@ private fun ReRecordButton(viewModel: VoiceReplyViewModel) { onRecordTaken = { recording -> viewModel.selectRecording(recording) }, - ) { isRecording, _ -> + ) { isRecording, elapsedSeconds -> + val contentColor = + if (isRecording) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onBackground + } + val icon = + if (isRecording) { + Icons.Default.Stop + } else { + Icons.Default.Mic + } + val label = + if (isRecording) { + formatSecondsToTime(elapsedSeconds) + } else { + stringRes(id = R.string.re_record) + } + val iconDescription = + if (isRecording) { + stringRes(id = R.string.recording_indicator_description) + } else { + stringRes(id = R.string.record_a_message) + } Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Icon( - imageVector = Icons.Default.Mic, - contentDescription = stringRes(id = R.string.record_a_message), - tint = - if (isRecording) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onBackground - }, + imageVector = icon, + contentDescription = iconDescription, + tint = contentColor, ) Text( - text = stringRes(id = R.string.re_record), - color = - if (isRecording) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onBackground - }, + text = label, + color = contentColor, ) } } From 7b6b0f66a5b449bbd08d7e9765df7c742dd3d54c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 17:49:02 +0100 Subject: [PATCH 08/15] guard for NaN crash with short recordings --- .../ui/components/AudioWaveformReadOnly.kt | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt index b6453b4cfe..490352912b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/AudioWaveformReadOnly.kt @@ -26,7 +26,6 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.requiredHeight -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -71,7 +70,6 @@ fun AudioWaveformReadOnly( progressBrush: Brush = SolidColor(Color.Blue), waveformAlignment: WaveformAlignment = WaveformAlignment.Center, amplitudeType: AmplitudeType = AmplitudeType.Avg, - onProgressChangeFinished: (() -> Unit)? = null, spikeAnimationSpec: AnimationSpec = tween(500), spikeWidth: Dp = 3.dp, spikeRadius: Dp = 2.dp, @@ -80,7 +78,6 @@ fun AudioWaveformReadOnly( amplitudes: List, onProgressChange: (Float) -> Unit, ) { - val backgroundColor = MaterialTheme.colorScheme.background val progressState = remember(progress) { progress.coerceIn(MIN_PROGRESS, MAX_PROGRESS) } val spikeWidthState = remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) } @@ -195,7 +192,20 @@ internal fun Iterable.chunkToSize( internal fun Iterable.normalize( min: Float, max: Float, -): List = map { (max - min) * ((it - min()) / (max() - min())) + min } +): List { + val values = toList() + if (values.isEmpty()) return emptyList() + + val currentMin = values.minOrNull() ?: return emptyList() + val currentMax = values.maxOrNull() ?: return emptyList() + val range = currentMax - currentMin + if (!range.isFinite() || range == 0f) { + return List(values.size) { min } + } + + val scale = max - min + return values.map { scale * ((it - currentMin) / range) + min } +} private fun Int.safeDiv(value: Int): Float { return if (value == 0) return 0F else this / value.toFloat() From d52906fda089343dc510767a90509db89e64c5e2 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 17:58:25 +0100 Subject: [PATCH 09/15] remove unused imports --- .../com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt | 1 - .../com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt index 304929af4a..67a70a5009 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt @@ -28,7 +28,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt index 12307ecd2e..43221805b7 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt +++ b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/TimeAgoFormatter.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.util -import com.sun.org.apache.xalan.internal.lib.ExsltDatetime.time import com.vitorpamplona.quartz.utils.TimeUtils import java.text.SimpleDateFormat import java.util.Locale From cf0f71d04f78cbb859f33d8762b3d2eab4c6c89d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 12:02:26 -0500 Subject: [PATCH 10/15] Fixes: https://github.com/vitorpamplona/amethyst/issues/1621 Renders the fork information at the second line for the Master Note as well. --- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../amethyst/ui/note/elements/ForkInfo.kt | 64 ++----------------- .../loggedIn/threadview/ThreadFeedView.kt | 51 ++++----------- 3 files changed, 16 insertions(+), 101 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 7956f2a8de..aa5928c7fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -1088,7 +1088,7 @@ fun SecondUserInfoRow( ) { Column(modifier = remember(noteEvent) { Modifier.weight(1f) }) { if (noteEvent is IForkableEvent && noteEvent.isAFork()) { - ShowForkInformation(noteEvent, remember(noteEvent) { Modifier.weight(1f) }, accountViewModel, nav) + ShowForkInformation(noteEvent, Modifier, accountViewModel, nav) } else { ObserveDisplayNip05Status(noteAuthor, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt index 2c52fdaa7d..b36093573c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/ForkInfo.kt @@ -21,32 +21,24 @@ package com.vitorpamplona.amethyst.ui.note.elements import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text 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.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUser -import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserInfo import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.LoadNote -import com.vitorpamplona.amethyst.ui.components.appendLink import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Font14SP -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.nip05 import com.vitorpamplona.quartz.experimental.forks.IForkableEvent @Composable @@ -86,30 +78,14 @@ fun ForkInformationRowLightColor( val route = remember(note) { routeFor(note, accountViewModel.account) } if (route != null) { - Row(modifier) { - Text( - text = - buildAnnotatedString { - appendLink(stringRes(id = R.string.forked_from) + " ") { - nav.nav(route) - } - }, - style = - LocalTextStyle.current.copy( - color = MaterialTheme.colorScheme.nip05, - fontSize = Font14SP, - ), - maxLines = 1, - overflow = TextOverflow.Visible, - ) - + Row(modifier, verticalAlignment = Alignment.CenterVertically) { val userState by observeUser(author, accountViewModel) userState?.user?.toBestDisplayName()?.let { CreateClickableTextWithEmoji( - clickablePart = it, + clickablePart = stringRes(id = R.string.forked_from) + " " + it, maxLines = 1, route = route, - overrideColor = MaterialTheme.colorScheme.nip05, + overrideColor = MaterialTheme.colorScheme.primary, fontSize = Font14SP, nav = nav, tags = userState?.user?.info?.tags, @@ -118,35 +94,3 @@ fun ForkInformationRowLightColor( } } } - -@Composable -fun ForkInformationRow( - originalVersion: Note, - modifier: Modifier = Modifier, - accountViewModel: AccountViewModel, - nav: INav, -) { - val noteState by observeNote(originalVersion, accountViewModel) - val note = noteState.note - val route = remember(note) { routeFor(note, accountViewModel.account) } - - if (route != null) { - Row(modifier) { - val author = note.author ?: return - val meta by observeUserInfo(author, accountViewModel) - - Text(stringRes(id = R.string.forked_from)) - Spacer(modifier = StdHorzSpacer) - - val userMetadata by observeUserInfo(author, accountViewModel) - - CreateClickableTextWithEmoji( - clickablePart = remember(meta) { meta?.bestName() ?: author.pubkeyDisplayHex() }, - maxLines = 1, - route = route, - nav = nav, - tags = userMetadata?.tags, - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt index b48551417b..e3260439e2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/threadview/ThreadFeedView.kt @@ -113,9 +113,9 @@ import com.vitorpamplona.amethyst.ui.note.elements.DisplayFollowingHashtagsInPos import com.vitorpamplona.amethyst.ui.note.elements.DisplayLocation import com.vitorpamplona.amethyst.ui.note.elements.DisplayPoW import com.vitorpamplona.amethyst.ui.note.elements.DisplayReward -import com.vitorpamplona.amethyst.ui.note.elements.ForkInformationRow import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton import com.vitorpamplona.amethyst.ui.note.elements.Reward +import com.vitorpamplona.amethyst.ui.note.elements.ShowForkInformation import com.vitorpamplona.amethyst.ui.note.elements.TimeAgo import com.vitorpamplona.amethyst.ui.note.observeEdits import com.vitorpamplona.amethyst.ui.note.showAmount @@ -187,11 +187,10 @@ import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent import com.vitorpamplona.quartz.experimental.bounties.bountyBaseReward import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent -import com.vitorpamplona.quartz.experimental.forks.forkFromAddress +import com.vitorpamplona.quartz.experimental.forks.IForkableEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.medical.FhirResourceEvent import com.vitorpamplona.quartz.experimental.nip95.header.FileStorageHeaderEvent -import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent import com.vitorpamplona.quartz.experimental.publicMessages.PublicMessageEvent import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent import com.vitorpamplona.quartz.nip01Core.core.Event @@ -509,11 +508,15 @@ private fun FullBleedNoteCompose( Column( remember { Modifier.weight(1f) }, ) { - ObserveDisplayNip05Status( - baseNote, - accountViewModel, - nav, - ) + if (noteEvent is IForkableEvent && noteEvent.isAFork()) { + ShowForkInformation(noteEvent, Modifier, accountViewModel, nav) + } else { + ObserveDisplayNip05Status( + baseNote, + accountViewModel, + nav, + ) + } } val geo = remember { noteEvent.geoHashOrScope() } @@ -546,7 +549,6 @@ private fun FullBleedNoteCompose( is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote, accountViewModel) is LongTextNoteEvent -> RenderLongFormHeaderForThread(noteEvent, baseNote, accountViewModel) is WikiNoteEvent -> RenderWikiHeaderForThread(noteEvent, accountViewModel, nav) - is NipTextEvent -> RenderNipHeaderForThread(noteEvent, accountViewModel, nav) is ClassifiedsEvent -> RenderClassifiedsReaderForThread(noteEvent, baseNote, accountViewModel, nav) } @@ -1110,8 +1112,6 @@ private fun RenderWikiHeaderForThread( accountViewModel: AccountViewModel, nav: INav, ) { - val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } - Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { Column { noteEvent.image()?.let { @@ -1137,14 +1137,6 @@ private fun RenderWikiHeaderForThread( ) } - forkedAddress?.let { - LoadAddressableNote(it, accountViewModel) { originalVersion -> - if (originalVersion != null) { - ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) - } - } - } - noteEvent .summary() ?.ifBlank { null } @@ -1159,24 +1151,3 @@ private fun RenderWikiHeaderForThread( } } } - -@Composable -private fun RenderNipHeaderForThread( - noteEvent: NipTextEvent, - accountViewModel: AccountViewModel, - nav: INav, -) { - val forkedAddress = remember(noteEvent) { noteEvent.forkFromAddress() } - - Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) { - Column { - forkedAddress?.let { - LoadAddressableNote(it, accountViewModel) { originalVersion -> - if (originalVersion != null) { - ForkInformationRow(originalVersion, Modifier.fillMaxWidth(), accountViewModel, nav) - } - } - } - } - } -} From a935c7cecf2c3a4bfbac35985a3f6f42e4c54f5e Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 10 Jan 2026 17:06:00 +0000 Subject: [PATCH 11/15] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 1 + amethyst/src/main/res/values-de-rDE/strings.xml | 1 + amethyst/src/main/res/values-pl-rPL/strings.xml | 4 +++- amethyst/src/main/res/values-pt-rBR/strings.xml | 3 +++ amethyst/src/main/res/values-sv-rSE/strings.xml | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 8ff4d900ae..7db2029cb6 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -673,6 +673,7 @@ Politika příspěvků Zásady ochrany soukromí Podmínky & ujednání + N/A Chyby a upozornění z tohoto relé Délka zprávy Předplatné diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index f31726918b..d1e1a9f3e3 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -678,6 +678,7 @@ anz der Bedingungen ist erforderlich Veröffentlichungsrichtlinie Datenschutzerklärung Allgemeine Geschäftsbedingungen + N/A Fehler und Hinweise von diesem Relais Nachrichtenlänge Abonnements diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index bb966d36bc..cb844defb6 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -64,7 +64,7 @@ Zapy Liczba wyświetleń Powtórz - powtórzony + powtórzono edytowano edytuj #%1$s oryginalny @@ -135,6 +135,8 @@ Dodaj użytkownika Dodaj Transmiter Imię + Imię (dla @tagging) + Moje imię @tag Nazwa użytkownika Mój nick G Braun diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index d3d01e9930..dcd1a2b458 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -673,6 +673,7 @@ Política de postagem Política de privacidade Termos & condições + N/A Erros e Avisos deste Relé Tamanho da mensagem Assinaturas @@ -686,6 +687,8 @@ Aceita até %1$s no futuro há %1$s + %1$s zeros + %1$s bits Retenção de eventos Tamanho do conteúdo Conectividade diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 623a0eadb0..74ffeae200 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -672,6 +672,7 @@ Publiceringspolicy Integritetspolicy Villkor & bestämmelser + N/A Fel och meddelanden från detta relä Meddelandelängd Prenumerationer From 30e5b3d35e7f926963e290ef6dd7aa671ba1750f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 12:08:42 -0500 Subject: [PATCH 12/15] removes bringing back all versions of an Addressable on the search screen. --- .../main/java/com/vitorpamplona/amethyst/model/LocalCache.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 ad2e47c4e6..f13edbb7fb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -2100,6 +2100,10 @@ object LocalCache : ILocalCache { } return notes.filter { _, note -> + if (note.event is AddressableEvent) { + return@filter false + } + if (excludeNoteEventFromSearchResults(note)) { return@filter false } From d24fe03bdd9ee6eaf9dc924713b413f2079375f8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sat, 10 Jan 2026 18:34:01 -0500 Subject: [PATCH 13/15] Avoids crashing when the k tag is a text --- .../com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt index 62569ff8da..623c817150 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/kinds/KindTag.kt @@ -44,7 +44,7 @@ class KindTag { ensure(tag.has(1)) { return null } ensure(tag[0] == TAG_NAME) { return null } ensure(tag[1].isNotEmpty()) { return null } - return tag[1].toInt() + return tag[1].toIntOrNull() } fun assemble(kind: Int) = arrayOf(TAG_NAME, kind.toString()) From d9a9cdd374bff9e45053ea8b1c02d16c615caf41 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 11 Jan 2026 12:18:21 -0500 Subject: [PATCH 14/15] Fixes lack of update in follow count on the UserProfile page --- .../service/relayClient/reqCommand/user/UserObservers.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt index d40d4fb695..d2d2b59dab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/user/UserObservers.kt @@ -249,15 +249,12 @@ fun observeUserFollowCount( remember(user) { user .flow() - .followers.stateFlow - .sample(200) - .mapLatest { userState -> - userState.user.transientFollowCount() ?: 0 - }.distinctUntilChanged() + .follows.stateFlow + .mapLatest { it.user.transientFollowCount() ?: 0 } .flowOn(Dispatchers.IO) } - return flow.collectAsStateWithLifecycle(0) + return flow.collectAsStateWithLifecycle(user.transientFollowCount() ?: 0) } @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) From b71bb8cd2b45cd7b65492cbc2957f4d3d818df16 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 11 Jan 2026 17:20:41 +0000 Subject: [PATCH 15/15] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 1 + amethyst/src/main/res/values-de-rDE/strings.xml | 1 + amethyst/src/main/res/values-hu-rHU/strings.xml | 3 +++ amethyst/src/main/res/values-pl-rPL/strings.xml | 1 + amethyst/src/main/res/values-pt-rBR/strings.xml | 1 + amethyst/src/main/res/values-sv-rSE/strings.xml | 1 + 6 files changed, 8 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 7db2029cb6..9605e4c53c 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1227,4 +1227,5 @@ Broadcast sady doporučení Smazat seznam Smazat sadu doporučení + Typy diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index d1e1a9f3e3..7a521e907e 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1232,4 +1232,5 @@ anz der Bedingungen ist erforderlich Empfehlungspaket veröffentlichen Liste löschen Empfehlungspaket löschen + Typen diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index c4fce36dba..d6716c99c6 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -135,6 +135,8 @@ Felhasználó ozzáadása Egy átjátszó hozzáadása Név + Név (az @említéshez) + Saját @említési név Megjelenítendő név Saját megjelenítendő név Strucc McNagyszerű @@ -1230,4 +1232,5 @@ Közvetítési csomag Lista törlése Csomag törlése + Változatok diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index cb844defb6..8d54348804 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -1229,4 +1229,5 @@ Upowszechnij kategorię Usuń listę Usuń kategorię + Typy diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index dcd1a2b458..a3f8b297c7 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1227,4 +1227,5 @@ Transmitir pacote de recomendações Excluir lista Excluir pacote de recomendações + Tipos diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 74ffeae200..7a56f1c6e3 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1226,4 +1226,5 @@ Sänd rekommendationspaket Ta bort lista Ta bort rekommendationspaket + Typer