From e874e302e06d14191d6ccf047b5e09f2e82dbc58 Mon Sep 17 00:00:00 2001 From: davotoula Date: Fri, 9 Jan 2026 11:04:34 +0100 Subject: [PATCH 01/70] 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 c32931f87..0cae372a8 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 2073348b2..dc8aebca6 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 6cbc364be..05c69d1a0 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 6939e8824..f4646c226 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/70] 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 0cae372a8..8ff4d900a 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 dc8aebca6..f31726918 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 9b9946c55..8fff23237 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 05c69d1a0..d3d01e993 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 f4646c226..623a0eadb 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 714143826..6cbc2e7d5 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/70] 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 1b452063f..0bad1548b 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 dd1cf3e62..3c69f4fad 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 f9db3f422..d707d87fc 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 b8fa57650..6ad3d4bad 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 000000000..557e26c46 --- /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 6cf7aea7d..9ee0b0632 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 ac8b756a2..ca20492aa 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 5b0c37659..307162570 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 8e9eddd2b..b7c3ef37c 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 46bebbc44..5ac4955e9 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 a914febf5..b96effdd4 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 fac9b7661..d07a6f284 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 dbbd86c0e..991ed0f80 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 22a90a03e..dbb4981bf 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 3b4b1b727..4aadd0277 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 e3f5068b9..b48551417 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 d033e2736..e9b34537c 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 000000000..47fddb817 --- /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 000000000..f00e84c3a --- /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 000000000..f77989cfa --- /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 000000000..27f9547fb --- /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 0d38a4f5b..aa57f067d 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 e58c8d1ce..b9fb1a08c 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 000000000..f460e981e --- /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 ebc6d3a16..e2908f2b4 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 ef11c9d3f..2d6162696 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/70] 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 0bad1548b..ad2e47c4e 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 d707d87fc..7956f2a8d 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 279a3414f..a2814a464 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 6ad3d4bad..2c52fdaa7 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 d07a6f284..b8a5ba010 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 47fddb817..e614cf866 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 cc2971896..714404777 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 f00e84c3a..07581a783 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 f50a5dd48..b8ac14da8 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 f460e981e..f6715d8fd 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 e2908f2b4..e81c56fbe 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/70] 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 c5083fb12..32af08e0b 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 6df79b5cd..a63656d4b 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/70] 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 a4e3ecd65..0f7fb0946 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/70] 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 da1dd4b1c..15f30bb19 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/70] 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 b6453b4cf..490352912 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/70] 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 304929af4..67a70a500 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 12307ecd2..43221805b 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/70] 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 7956f2a8d..aa5928c7f 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 2c52fdaa7..b36093573 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 b48551417..e3260439e 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/70] 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 8ff4d900a..7db2029cb 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 f31726918..d1e1a9f3e 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 bb966d36b..cb844defb 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 d3d01e993..dcd1a2b45 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 623a0eadb..74ffeae20 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/70] 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 ad2e47c4e..f13edbb7f 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/70] 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 62569ff8d..623c81715 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 58b97e069506f20ac342cb5ff32b41a079c74e62 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sat, 10 Jan 2026 18:47:19 +0100 Subject: [PATCH 14/70] =?UTF-8?q?Voice=20reply=20to=20VoiceEvent/VoiceRepl?= =?UTF-8?q?yEvent=20(KIND=201222/1244)=20=E2=86=92=20Creates=20VoiceReplyE?= =?UTF-8?q?vent=20(KIND=201244)=20Voice=20reply=20to=20TextNoteEvent=20(KI?= =?UTF-8?q?ND=201)=20=E2=86=92=20Creates=20TextNoteEvent=20(KIND=201)=20wi?= =?UTF-8?q?th=20audio=20IMeta=20attachment=20and=20proper=20reply=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/screen/loggedIn/AccountViewModel.kt | 49 +++++++++++++++---- .../loggedIn/home/ShortNotePostViewModel.kt | 43 +++++++++++++--- .../loggedIn/home/VoiceReplyViewModel.kt | 36 +++++++++++--- 3 files changed, 105 insertions(+), 23 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 9c983e475..085653a95 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -107,6 +107,9 @@ import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -134,6 +137,7 @@ import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag +import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log @@ -1154,8 +1158,6 @@ class AccountViewModel( context: Context, ) { if (isWriteable()) { - val hint = note.toEventHint() ?: return - launchSigner { val uploader = UploadOrchestrator() val result = @@ -1171,14 +1173,41 @@ class AccountViewModel( ) if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - account.sendVoiceReplyMessage( - result.result.url, - result.result.fileHeader.mimeType ?: recording.mimeType, - result.result.fileHeader.hash, - recording.duration, - recording.amplitudes, - hint, - ) + val audioMeta = + AudioMeta( + url = result.result.url, + mimeType = result.result.fileHeader.mimeType ?: recording.mimeType, + hash = result.result.fileHeader.hash, + duration = recording.duration, + waveform = recording.amplitudes, + ) + + // Check if replying to a voice event + val voiceHint = note.toEventHint() + if (voiceHint != null) { + // Create VoiceReplyEvent (KIND 1244) for voice-to-voice replies + account.sendVoiceReplyMessage( + result.result.url, + result.result.fileHeader.mimeType ?: recording.mimeType, + result.result.fileHeader.hash, + recording.duration, + recording.amplitudes, + voiceHint, + ) + } else { + // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes + val template = + TextNoteEvent.build(audioMeta.url) { + val replyingTo = note.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } + account.signAndComputeBroadcast(template) + } } else if (result is UploadingState.Error) { toastManager.toast( R.string.failed_to_upload_media_no_details, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index cd479b134..8495a3083 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -542,18 +542,49 @@ open class ShortNotePostViewModel : voiceMetadata?.let { audioMeta -> // Only create voice reply if original note is also a voice message val originalVoiceHint = originalNote?.toEventHint() - return if (originalVoiceHint != null) { - // Create voice reply event - VoiceReplyEvent.build( + if (originalVoiceHint != null) { + // Create voice reply event (KIND 1244) + return VoiceReplyEvent.build( voiceMessage = audioMeta, replyingTo = originalVoiceHint, ) - } else { - // Create root voice event (no reply or original is not a voice message) - VoiceEvent.build( + } + // If no original note, create a standalone voice event (KIND 1222) + if (originalNote == null) { + return VoiceEvent.build( voiceMessage = audioMeta, ) } + // Otherwise, original note exists but is not a voice message + // Create a TextNoteEvent (KIND 1) with audio as IMeta attachment + return TextNoteEvent.build(audioMeta.url) { + val replyingTo = originalNote?.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + tags.forEach { + val note = accountViewModel.getNoteIfExists(it.eventId) + val ourAuthor = note?.author?.pubkeyHex + val ourHint = note?.relayHintUrl() + if (it.author == null || it.author?.isBlank() == true) { + it.author = ourAuthor + } else { + if (ourAuthor != null && it.author != ourAuthor) { + it.author = ourAuthor + } + } + if (it.relay == null) { + it.relay = ourHint + } else { + if (ourHint != null && it.relay != ourHint) { + it.relay = ourHint + } + } + } + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } } val tagger = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index faa5d677a..097aac737 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -37,6 +37,10 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent @@ -200,12 +204,6 @@ class VoiceReplyViewModel : ViewModel() { is UploadingState.Finished -> { when (val orchestratorResult = result.result) { is UploadOrchestrator.OrchestratorResult.ServerResult -> { - val hint = note.toEventHint() - if (hint == null) { - accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed) - return - } - val audioMeta = AudioMeta( url = orchestratorResult.url, @@ -215,7 +213,31 @@ class VoiceReplyViewModel : ViewModel() { waveform = recording.amplitudes, ) - accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, hint)) + // Check if replying to a voice event + val voiceHint = note.toEventHint() + if (voiceHint != null) { + // Create VoiceReplyEvent (KIND 1244) for voice-to-voice replies + accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, voiceHint)) + } else { + // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes + val textHint = note.toEventHint() + if (textHint == null) { + accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed) + return + } + + val template = + TextNoteEvent.build(audioMeta.url) { + val replyingTo = note.toEventHint() + if (replyingTo != null) { + val tags = prepareETagsAsReplyTo(replyingTo, null) + markedETags(tags) + } + // Add audio as IMeta attachment + add(audioMeta.toIMetaArray()) + } + accountViewModel.account.signAndComputeBroadcast(template) + } if (server.type != ServerType.NIP95) { accountViewModel.account.settings.changeDefaultFileServer(server) From d8093f64b10044d3199a1270099cad4442466672 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 10:27:12 +0100 Subject: [PATCH 15/70] Display kind 1 voice replies as audio waveform --- .../amethyst/ui/note/types/Text.kt | 7 ++ .../amethyst/ui/note/types/VoiceTrack.kt | 84 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 8432deaa2..1a35de21e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -68,6 +68,13 @@ fun RenderTextEvent( ) { val noteEvent = note.event ?: return + // Check if this is an audio-only event (content is just an audio URL with waveform IMeta) + val isAudioOnly = remember(noteEvent) { noteEvent.isAudioOnlyContent() } + if (isAudioOnly) { + RenderAudioFromIMeta(note, accountViewModel, nav) + return + } + val showReply by remember(note) { derivedStateOf { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 0a0210fb0..34939c9a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -69,8 +69,11 @@ import com.vitorpamplona.amethyst.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.ui.theme.Size75Modifier import com.vitorpamplona.amethyst.ui.theme.VoiceHeightModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hasHashtags import com.vitorpamplona.quartz.nip14Subject.subject +import com.vitorpamplona.quartz.nip92IMeta.imetas +import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent @Composable @@ -263,3 +266,84 @@ fun PlayPauseButton(controllerState: MediaControllerState) { } } } + +/** + * Extracts AudioMeta from an event's IMeta tags if it has audio content with waveform. + * Returns the first audio IMeta that has a waveform, or null if none found. + */ +fun Event.getAudioMetaWithWaveform(): AudioMeta? { + val audioMetas = imetas().map { AudioMeta.parse(it) } + return audioMetas.firstOrNull { meta -> + meta.waveform != null && + meta.mimeType?.startsWith("audio/") == true + } +} + +/** + * Checks if the event content is primarily an audio attachment (content is just the audio URL). + */ +fun Event.isAudioOnlyContent(): Boolean { + val audioMeta = getAudioMetaWithWaveform() ?: return false + return content.trim() == audioMeta.url +} + +/** + * Renders audio with waveform for any event type that has audio IMeta attachment. + * This allows KIND 1 (TextNoteEvent) and KIND 1111 (CommentEvent) with voice + * attachments to display the same waveform UI as VoiceEvent. + */ +@Composable +fun RenderAudioFromIMeta( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event ?: return + + val audioMeta = remember(noteEvent) { noteEvent.getAudioMetaWithWaveform() } ?: return + + val waveform = + remember(audioMeta) { + audioMeta.waveform?.let { WaveformData(it) } + } + + val callbackUri = remember(note) { note.toNostrUri() } + + Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + GetMediaItem( + videoUri = audioMeta.url, + title = null, + artworkUri = null, + authorName = note.author?.toBestDisplayName(), + callbackUri = callbackUri, + mimeType = audioMeta.mimeType, + aspectRatio = null, + proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(audioMeta.url), + keepPlaying = false, + waveformData = waveform, + ) { mediaItem -> + GetVideoController( + mediaItem = mediaItem, + muted = false, + ) { controller -> + RenderVoicePlayer( + mediaItem = mediaItem, + controllerState = controller, + waveform = waveform, + borderModifier = MaterialTheme.colorScheme.imageModifier, + accountViewModel = accountViewModel, + ) + } + } + } + + if (noteEvent.hasHashtags()) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav) + } + } + } +} From 70c638ad9043f55be8ec393f46af3858766d8bc7 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 10:33:05 +0100 Subject: [PATCH 16/70] refactor to a shared composable to eliminate waveform code duplication --- .../amethyst/ui/note/types/VoiceTrack.kt | 84 +++++++++---------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 34939c9a8..632d6775b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -110,6 +110,32 @@ fun VoiceHeader( ?.let { WaveformData(it) } } + RenderAudioWithWaveform( + mediaUrl = media, + title = noteEvent.subject(), + mimeType = null, + waveform = waveform, + note = note, + accountViewModel = accountViewModel, + nav = nav, + ) +} + +/** + * Shared composable for rendering audio with waveform visualization. + * Used by both VoiceHeader (for BaseVoiceEvent) and RenderAudioFromIMeta (for other events with audio IMeta). + */ +@Composable +fun RenderAudioWithWaveform( + mediaUrl: String, + title: String?, + mimeType: String?, + waveform: WaveformData?, + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val noteEvent = note.event ?: return val callbackUri = remember(note) { note.toNostrUri() } Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) { @@ -117,14 +143,14 @@ fun VoiceHeader( verticalAlignment = Alignment.CenterVertically, ) { GetMediaItem( - videoUri = media, - title = noteEvent.subject(), + videoUri = mediaUrl, + title = title, artworkUri = null, authorName = note.author?.toBestDisplayName(), callbackUri = callbackUri, - mimeType = null, + mimeType = mimeType, aspectRatio = null, - proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(media), + proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(mediaUrl), keepPlaying = false, waveformData = waveform, ) { mediaItem -> @@ -171,7 +197,7 @@ fun RenderVoicePlayer( factory = { context: Context -> PlayerView(context).apply { player = controllerState.controller - // if we alrady know the size of the frame, this forces the player to stay in the size + // if we already know the size of the frame, this forces the player to stay in the size layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, @@ -307,43 +333,13 @@ fun RenderAudioFromIMeta( audioMeta.waveform?.let { WaveformData(it) } } - val callbackUri = remember(note) { note.toNostrUri() } - - Column(modifier = MaxWidthPaddingTop5dp, horizontalAlignment = Alignment.CenterHorizontally) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - GetMediaItem( - videoUri = audioMeta.url, - title = null, - artworkUri = null, - authorName = note.author?.toBestDisplayName(), - callbackUri = callbackUri, - mimeType = audioMeta.mimeType, - aspectRatio = null, - proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(audioMeta.url), - keepPlaying = false, - waveformData = waveform, - ) { mediaItem -> - GetVideoController( - mediaItem = mediaItem, - muted = false, - ) { controller -> - RenderVoicePlayer( - mediaItem = mediaItem, - controllerState = controller, - waveform = waveform, - borderModifier = MaterialTheme.colorScheme.imageModifier, - accountViewModel = accountViewModel, - ) - } - } - } - - if (noteEvent.hasHashtags()) { - Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - DisplayUncitedHashtags(noteEvent, callbackUri, accountViewModel, nav) - } - } - } + RenderAudioWithWaveform( + mediaUrl = audioMeta.url, + title = null, + mimeType = audioMeta.mimeType, + waveform = waveform, + note = note, + accountViewModel = accountViewModel, + nav = nav, + ) } From f51d0fb56e5741fffea45c7f7c48d27da5f8dd03 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 17:39:08 +0100 Subject: [PATCH 17/70] code review: added p-tag for voice reply to kind 1 allow waveform IMeta with a missing mimeType to render as audio --- .../vitorpamplona/amethyst/ui/note/types/Text.kt | 14 +++++++------- .../amethyst/ui/note/types/VoiceTrack.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 15 ++++++++------- .../loggedIn/home/ShortNotePostViewModel.kt | 12 ++++++++++++ .../screen/loggedIn/home/VoiceReplyViewModel.kt | 3 +++ 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt index 1a35de21e..76c68e29b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Text.kt @@ -68,13 +68,6 @@ fun RenderTextEvent( ) { val noteEvent = note.event ?: return - // Check if this is an audio-only event (content is just an audio URL with waveform IMeta) - val isAudioOnly = remember(noteEvent) { noteEvent.isAudioOnlyContent() } - if (isAudioOnly) { - RenderAudioFromIMeta(note, accountViewModel, nav) - return - } - val showReply by remember(note) { derivedStateOf { @@ -107,6 +100,13 @@ fun RenderTextEvent( } } + // Check if this is an audio-only event (content is just an audio URL with waveform IMeta) + val isAudioOnly = remember(noteEvent) { noteEvent.isAudioOnlyContent() } + if (isAudioOnly) { + RenderAudioFromIMeta(note, accountViewModel, nav) + return + } + LoadDecryptedContent( note, accountViewModel, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 632d6775b..1475f1e36 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -301,7 +301,7 @@ fun Event.getAudioMetaWithWaveform(): AudioMeta? { val audioMetas = imetas().map { AudioMeta.parse(it) } return audioMetas.firstOrNull { meta -> meta.waveform != null && - meta.mimeType?.startsWith("audio/") == true + meta.mimeType?.startsWith("audio/") != false } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 085653a95..a439463d0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -105,10 +105,12 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser +import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.notify import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent @@ -394,7 +396,7 @@ class AccountViewModel( note.flow().author(), note.flow().metadata.stateFlow, note.flow().reports.stateFlow, - ) { hiddenUsers, followingUsers, autor, metadata, reports -> + ) { hiddenUsers, followingUsers, _, metadata, _ -> emit(isNoteAcceptable(metadata.note, hiddenUsers, followingUsers.authors)) }.onStart { emit( @@ -1015,11 +1017,11 @@ class AccountViewModel( LocalCache.findLatestModificationForNote(note) } - fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel? = LocalCache.getOrCreatePublicChatChannel(key) + fun checkGetOrCreatePublicChatChannel(key: HexKey): PublicChatChannel = LocalCache.getOrCreatePublicChatChannel(key) - fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel? = LocalCache.getOrCreateLiveChannel(key) + fun checkGetOrCreateLiveActivityChannel(key: Address): LiveActivitiesChannel = LocalCache.getOrCreateLiveChannel(key) - fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel? = LocalCache.getOrCreateEphemeralChannel(key) + fun checkGetOrCreateEphemeralChatChannel(key: RoomId): EphemeralChatChannel = LocalCache.getOrCreateEphemeralChannel(key) fun getPublicChatChannelIfExists(hex: HexKey) = LocalCache.getPublicChatChannelIfExists(hex) @@ -1202,6 +1204,7 @@ class AccountViewModel( if (replyingTo != null) { val tags = prepareETagsAsReplyTo(replyingTo, null) markedETags(tags) + notify(replyingTo.toPTag()) } // Add audio as IMeta attachment add(audioMeta.toIMetaArray()) @@ -1451,7 +1454,7 @@ class AccountViewModel( // First check if we have an actual response from the DVM in LocalCache val response = LocalCache.notes.maxOrNullOf( - filter = { key, note -> + filter = { _, note -> val noteEvent = note.event noteEvent is NIP90ContentDiscoveryResponseEvent && noteEvent.pubKey == pubkeyHex && @@ -1560,8 +1563,6 @@ class AccountViewModel( } } - fun findUsersStartingWithSync(prefix: String) = LocalCache.findUsersStartingWith(prefix, account) - fun convertAccounts(loggedInAccounts: List?): Set = loggedInAccounts ?.mapNotNull { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 8495a3083..8eec5fd70 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -582,6 +582,18 @@ open class ShortNotePostViewModel : } markedETags(tags) } + pTags?.let { userList -> + val tags = + userList.map { + val tag = it.toPTag() + if (tag.relayHint == null) { + tag.copy(relayHint = LocalCache.relayHints.hintsForKey(it.pubkeyHex).firstOrNull()) + } else { + tag + } + } + notify(tags) + } // Add audio as IMeta attachment add(audioMeta.toIMetaArray()) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 097aac737..a34e4f1cf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -38,8 +38,10 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.markedETags +import com.vitorpamplona.quartz.nip10Notes.tags.notify import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent @@ -232,6 +234,7 @@ class VoiceReplyViewModel : ViewModel() { if (replyingTo != null) { val tags = prepareETagsAsReplyTo(replyingTo, null) markedETags(tags) + notify(replyingTo.toPTag()) } // Add audio as IMeta attachment add(audioMeta.toIMetaArray()) From d9a9cdd374bff9e45053ea8b1c02d16c615caf41 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Sun, 11 Jan 2026 12:18:21 -0500 Subject: [PATCH 18/70] 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 d40d4fb69..d2d2b59da 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 19/70] 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 7db2029cb..9605e4c53 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 d1e1a9f3e..7a521e907 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 c4fce36db..d6716c99c 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 cb844defb..8d5434880 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 dcd1a2b45..a3f8b297c 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 74ffeae20..7a56f1c6e 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 From 1e898d57b2a4d9ae6e22bd74d56c9adf56e1d7f2 Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 18:34:59 +0100 Subject: [PATCH 20/70] code review fixes: textHint variable was retrieved but never used extracted logic for fixing relay hints and author information in tags made mimeType check logic clearer --- .../amethyst/ui/note/types/VoiceTrack.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 112 +++++------------- .../loggedIn/home/ShortNotePostViewModel.kt | 22 +--- .../loggedIn/home/VoiceReplyViewModel.kt | 13 +- 4 files changed, 36 insertions(+), 113 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt index 1475f1e36..6f4d34a2b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/VoiceTrack.kt @@ -301,7 +301,7 @@ fun Event.getAudioMetaWithWaveform(): AudioMeta? { val audioMetas = imetas().map { AudioMeta.parse(it) } return audioMetas.firstOrNull { meta -> meta.waveform != null && - meta.mimeType?.startsWith("audio/") != false + (meta.mimeType == null || meta.mimeType?.startsWith("audio/") == true) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index a439463d0..b24b67d58 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.rememberCoroutineScope -import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -67,12 +66,8 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.RelaySubscriptionsCoordinator import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler -import com.vitorpamplona.amethyst.service.uploads.CompressorQuality -import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator -import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.Dao import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.components.UrlPreviewState import com.vitorpamplona.amethyst.ui.components.toasts.ToastManager import com.vitorpamplona.amethyst.ui.feeds.FeedState @@ -105,13 +100,9 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser -import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip03Timestamp.EmptyOtsResolverBuilder import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent -import com.vitorpamplona.quartz.nip10Notes.tags.markedETags -import com.vitorpamplona.quartz.nip10Notes.tags.notify -import com.vitorpamplona.quartz.nip10Notes.tags.prepareETagsAsReplyTo +import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.RepostEvent @@ -139,8 +130,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import com.vitorpamplona.quartz.nip90Dvms.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.nip94FileMetadata.tags.DimensionTag -import com.vitorpamplona.quartz.nipA0VoiceMessages.AudioMeta -import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils @@ -1004,6 +993,32 @@ class AccountViewModel( fun getNoteIfExists(hex: HexKey): Note? = LocalCache.getNoteIfExists(hex) + /** + * Fixes author and relay hints in MarkedETag list by looking up notes from cache. + * This ensures reply tags have proper author pubkeys and relay hints for threading. + */ + fun fixReplyTagHints(tags: List) { + tags.forEach { tag -> + val note = getNoteIfExists(tag.eventId) + val cachedAuthor = note?.author?.pubkeyHex + val cachedRelay = note?.relayHintUrl() + + // Fix author if missing or different from cached + if (tag.author.isNullOrBlank() && cachedAuthor != null) { + tag.author = cachedAuthor + } else if (cachedAuthor != null && tag.author != cachedAuthor) { + tag.author = cachedAuthor + } + + // Fix relay hint if missing or different from cached + if (tag.relay == null && cachedRelay != null) { + tag.relay = cachedRelay + } else if (cachedRelay != null && tag.relay != cachedRelay) { + tag.relay = cachedRelay + } + } + } + override suspend fun getOrCreateAddressableNote(address: Address): AddressableNote = LocalCache.getOrCreateAddressableNote(address) fun getAddressableNoteIfExists(key: String): AddressableNote? = LocalCache.getAddressableNoteIfExists(key) @@ -1154,79 +1169,6 @@ class AccountViewModel( super.onCleared() } - fun sendVoiceReply( - note: Note, - recording: RecordingResult, - context: Context, - ) { - if (isWriteable()) { - launchSigner { - val uploader = UploadOrchestrator() - val result = - uploader.upload( - uri = recording.file.toUri(), - mimeType = recording.mimeType, - alt = null, - contentWarningReason = null, - compressionQuality = CompressorQuality.UNCOMPRESSED, - server = account.settings.defaultFileServer, - account = account, - context = context, - ) - - if (result is UploadingState.Finished && result.result is UploadOrchestrator.OrchestratorResult.ServerResult) { - val audioMeta = - AudioMeta( - url = result.result.url, - mimeType = result.result.fileHeader.mimeType ?: recording.mimeType, - hash = result.result.fileHeader.hash, - duration = recording.duration, - waveform = recording.amplitudes, - ) - - // Check if replying to a voice event - val voiceHint = note.toEventHint() - if (voiceHint != null) { - // Create VoiceReplyEvent (KIND 1244) for voice-to-voice replies - account.sendVoiceReplyMessage( - result.result.url, - result.result.fileHeader.mimeType ?: recording.mimeType, - result.result.fileHeader.hash, - recording.duration, - recording.amplitudes, - voiceHint, - ) - } else { - // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes - val template = - TextNoteEvent.build(audioMeta.url) { - val replyingTo = note.toEventHint() - if (replyingTo != null) { - val tags = prepareETagsAsReplyTo(replyingTo, null) - markedETags(tags) - notify(replyingTo.toPTag()) - } - // Add audio as IMeta attachment - add(audioMeta.toIMetaArray()) - } - account.signAndComputeBroadcast(template) - } - } else if (result is UploadingState.Error) { - toastManager.toast( - R.string.failed_to_upload_media_no_details, - result.errorResource, - *result.params, - ) - } - } - } else { - toastManager.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_reply, - ) - } - } - fun loadThumb( context: Context, thumbUri: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 8eec5fd70..d810088ed 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -83,6 +83,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.geohash.geohash import com.vitorpamplona.quartz.nip01Core.tags.geohash.getGeoHash import com.vitorpamplona.quartz.nip01Core.tags.hashtags.hashtags import com.vitorpamplona.quartz.nip01Core.tags.people.pTags +import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip01Core.tags.references.references import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent @@ -561,26 +562,9 @@ open class ShortNotePostViewModel : val replyingTo = originalNote?.toEventHint() if (replyingTo != null) { val tags = prepareETagsAsReplyTo(replyingTo, null) - tags.forEach { - val note = accountViewModel.getNoteIfExists(it.eventId) - val ourAuthor = note?.author?.pubkeyHex - val ourHint = note?.relayHintUrl() - if (it.author == null || it.author?.isBlank() == true) { - it.author = ourAuthor - } else { - if (ourAuthor != null && it.author != ourAuthor) { - it.author = ourAuthor - } - } - if (it.relay == null) { - it.relay = ourHint - } else { - if (ourHint != null && it.relay != ourHint) { - it.relay = ourHint - } - } - } + accountViewModel.fixReplyTagHints(tags) markedETags(tags) + notify(replyingTo.toPTag()) } pTags?.let { userList -> val tags = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index a34e4f1cf..3bd707f81 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -37,7 +37,6 @@ import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent import com.vitorpamplona.quartz.nip10Notes.tags.markedETags @@ -222,7 +221,7 @@ class VoiceReplyViewModel : ViewModel() { accountViewModel.account.signAndComputeBroadcast(VoiceReplyEvent.build(audioMeta, voiceHint)) } else { // Create TextNoteEvent (KIND 1) with audio IMeta for voice replies to regular notes - val textHint = note.toEventHint() + val textHint = note.toEventHint() if (textHint == null) { accountViewModel.toastManager.toast(uploadErrorTitle, uploadVoiceFailed) return @@ -230,12 +229,10 @@ class VoiceReplyViewModel : ViewModel() { val template = TextNoteEvent.build(audioMeta.url) { - val replyingTo = note.toEventHint() - if (replyingTo != null) { - val tags = prepareETagsAsReplyTo(replyingTo, null) - markedETags(tags) - notify(replyingTo.toPTag()) - } + val tags = prepareETagsAsReplyTo(textHint, null) + accountViewModel.fixReplyTagHints(tags) + markedETags(tags) + notify(textHint.toPTag()) // Add audio as IMeta attachment add(audioMeta.toIMetaArray()) } From e561492705bcd347b98046862adf8b9e765641da Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Jan 2026 15:28:33 -0500 Subject: [PATCH 21/70] Removes unused aliases --- amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index d203c59e5..132811377 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -24,6 +24,4 @@ package com.vitorpamplona.amethyst.model typealias Note = com.vitorpamplona.amethyst.commons.model.Note typealias NotesGatherer = com.vitorpamplona.amethyst.commons.model.NotesGatherer typealias AddressableNote = com.vitorpamplona.amethyst.commons.model.AddressableNote -typealias NoteFlowSet = com.vitorpamplona.amethyst.commons.model.NoteFlowSet -typealias NoteBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.NoteBundledRefresherFlow typealias NoteState = com.vitorpamplona.amethyst.commons.model.NoteState From e9210872bdd84ddb0d06ba717a7cfb72aecb78b9 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 12 Jan 2026 15:29:33 -0500 Subject: [PATCH 22/70] Improves performance by precaching signatures before sorting them Fixes out of order crash when the same signature came back. --- .../commons/viewmodels/thread/ThreadFeedFilter.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt index 4f96bc0e6..2d9200179 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/viewmodels/thread/ThreadFeedFilter.kt @@ -64,9 +64,8 @@ class ThreadFeedFilter( val eventsInHex = filteredThreadInfo.allNotes.map { it.idHex }.toSet() val now = TimeUtils.now() - // Currently orders by date of each event, descending, at each level of the reply stack - val order = - compareByDescending { + val signatures = + filteredThreadInfo.allNotes.associateWith { ThreadLevelCalculator .replyLevelSignature( it, @@ -78,6 +77,11 @@ class ThreadFeedFilter( ).signature } + // Currently orders by date of each event, descending, at each level of the reply stack + val order = + compareByDescending { signatures[it] } + .thenBy { it.idHex } + return filteredThreadInfo.allNotes.sortedWith(order) } } From c585b058793972d6f5bb250113339f6cfc753d01 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 12 Jan 2026 20:32:01 +0000 Subject: [PATCH 23/70] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 9605e4c53..275889023 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -30,7 +30,7 @@ Kopírovat text Kopírovat ID autora Kopírovat ID poznámky - Vysílání + Rozšířit na relé Časové razítko Časové razítko: Čeká na potvrzení OTS: Čeká @@ -114,7 +114,7 @@ "O nás…" Co vás napadá? Napište zprávu… - Příspěvek + Publikovat Uložit Vytvořit Přejmenovat @@ -741,7 +741,7 @@ Pouze Wi-Fi Neomezená WiFi Nikdy - Hotovo + Kompletní Zjednodušené Výkonost Klasický From bfd0fc01b94f400d91dfb45386b7ec665a280b9c Mon Sep 17 00:00:00 2001 From: davotoula Date: Sun, 11 Jan 2026 19:08:47 +0100 Subject: [PATCH 24/70] add TarsosDSP dependency for voice anonymization --- amethyst/build.gradle | 3 +++ gradle/libs.versions.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/amethyst/build.gradle b/amethyst/build.gradle index ee42fb543..2df5a4149 100644 --- a/amethyst/build.gradle +++ b/amethyst/build.gradle @@ -348,6 +348,9 @@ dependencies { // Image compression lib implementation libs.zelory.image.compressor + // Voice anonymization DSP + implementation libs.tarsosdsp + // Cbor for cashuB format implementation libs.kotlinx.serialization.cbor diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f1c53630..8f26e8b51 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -49,6 +49,7 @@ rfc3986 = "0.1.2" secp256k1KmpJniAndroid = "0.22.0" securityCryptoKtx = "1.1.0" spotless = "8.1.0" +tarsosdsp = "2.5" torAndroid = "0.4.8.21.1" translate = "17.0.3" unifiedpush = "3.0.10" @@ -145,6 +146,7 @@ rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc secp256k1-kmp-common = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" } secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" } +tarsosdsp = { group = "be.tarsos.dsp", name = "core", version.ref = "tarsosdsp" } tor-android = { module = "info.guardianproject:tor-android", version.ref = "torAndroid" } unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" } url-detector = { group = "io.github.url-detector", name = "url-detector", version.ref = "urlDetector" } From 83d4d3e756befeedbfe5db17ee416f47bcfdd810 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 11:44:40 +0100 Subject: [PATCH 25/70] add VoicePreset enum for voice anonymization Define preset options (None, Deep, High, Neutral) --- .../ui/actions/uploads/VoicePreset.kt | 35 +++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 4 +++ settings.gradle | 1 + 3 files changed, 40 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt new file mode 100644 index 000000000..1d16a3c8e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt @@ -0,0 +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.amethyst.ui.actions.uploads + +import androidx.annotation.StringRes +import com.vitorpamplona.amethyst.R + +enum class VoicePreset( + val pitchFactor: Double, + val formantShift: Double, + @StringRes val labelRes: Int, +) { + NONE(1.0, 0.0, R.string.voice_preset_none), + DEEP(0.75, -3.0, R.string.voice_preset_deep), + HIGH(1.4, 4.0, R.string.voice_preset_high), + NEUTRAL(1.0, -2.0, R.string.voice_preset_neutral), +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index e9b34537c..ebec66ae8 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -184,6 +184,10 @@ Unexpected upload state NIP-95 is not supported for voice messages yet Voice upload failed: %1$s + None + Deep + High + Neutral User does not have a lightning address set up to receive sats "reply here.. " Copies the Note ID to the clipboard for sharing in Nostr diff --git a/settings.gradle b/settings.gradle index cad6e3772..5603e440e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,6 +25,7 @@ dependencyResolutionManagement { mavenCentral() maven { url = "https://jitpack.io" } maven { url = "https://raw.githubusercontent.com/guardianproject/gpmaven/master" } + maven { url = "https://mvn.0110.be/releases" } } } From 8de148e56248b96b80fa9c86ce8aacfa6f1df1ce Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 11:59:53 +0100 Subject: [PATCH 26/70] add VoiceAnonymizer for audio pitch/formant shifting --- .../ui/actions/uploads/VoiceAnonymizer.kt | 435 ++++++++++++++++++ 1 file changed, 435 insertions(+) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt new file mode 100644 index 000000000..08d15c859 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -0,0 +1,435 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.uploads + +import android.media.MediaCodec +import android.media.MediaCodecInfo +import android.media.MediaExtractor +import android.media.MediaFormat +import android.media.MediaMuxer +import android.util.Log +import be.tarsos.dsp.AudioDispatcher +import be.tarsos.dsp.AudioEvent +import be.tarsos.dsp.AudioProcessor +import be.tarsos.dsp.WaveformSimilarityBasedOverlapAdd +import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter +import be.tarsos.dsp.io.TarsosDSPAudioFormat +import be.tarsos.dsp.resample.RateTransposer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext +import java.io.File +import java.nio.ByteOrder +import kotlin.coroutines.coroutineContext +import kotlin.math.abs + +data class AnonymizedResult( + val file: File, + val waveform: List, + val duration: Int, +) + +class VoiceAnonymizer { + companion object { + private const val TAG = "VoiceAnonymizer" + private const val SAMPLE_RATE = 44100 + private const val CHANNELS = 1 + private const val BIT_RATE = 128000 + } + + suspend fun anonymize( + inputFile: File, + preset: VoicePreset, + onProgress: (Float) -> Unit = {}, + ): Result = + withContext(Dispatchers.IO) { + if (preset == VoicePreset.NONE) { + return@withContext Result.failure( + IllegalArgumentException("Cannot anonymize with NONE preset"), + ) + } + + try { + val outputFile = createOutputFile(inputFile, preset) + val (pcmData, sampleRate, duration) = + decodeAudioToPcm(inputFile) { progress -> + onProgress(progress * 0.3f) + } + + val processedPcm = + processPcmWithTarsos(pcmData, preset, sampleRate) { progress -> + onProgress(0.3f + progress * 0.4f) + } + + val waveform = extractWaveform(processedPcm, sampleRate) + + encodePcmToAac(processedPcm, sampleRate, outputFile) { progress -> + onProgress(0.7f + progress * 0.3f) + } + + onProgress(1f) + Result.success(AnonymizedResult(outputFile, waveform, duration)) + } catch (e: Exception) { + Log.e(TAG, "Failed to anonymize audio", e) + Result.failure(e) + } + } + + private fun createOutputFile( + inputFile: File, + preset: VoicePreset, + ): File { + val baseName = inputFile.nameWithoutExtension + val presetSuffix = preset.name.lowercase() + return File(inputFile.parentFile, "${baseName}_$presetSuffix.mp4") + } + + private data class DecodedAudio( + val pcmData: FloatArray, + val sampleRate: Int, + val duration: Int, + ) + + private suspend fun decodeAudioToPcm( + inputFile: File, + onProgress: (Float) -> Unit, + ): DecodedAudio { + val extractor = MediaExtractor() + extractor.setDataSource(inputFile.absolutePath) + + var audioTrackIndex = -1 + var format: MediaFormat? = null + for (i in 0 until extractor.trackCount) { + val trackFormat = extractor.getTrackFormat(i) + val mime = trackFormat.getString(MediaFormat.KEY_MIME) + if (mime?.startsWith("audio/") == true) { + audioTrackIndex = i + format = trackFormat + break + } + } + + if (audioTrackIndex == -1 || format == null) { + extractor.release() + throw IllegalStateException("No audio track found in file") + } + + extractor.selectTrack(audioTrackIndex) + val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm" + val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + val durationUs = format.getLong(MediaFormat.KEY_DURATION) + val duration = (durationUs / 1_000_000).toInt() + + val decoder = MediaCodec.createDecoderByType(mime) + decoder.configure(format, null, null, 0) + decoder.start() + + val pcmSamples = mutableListOf() + val bufferInfo = MediaCodec.BufferInfo() + var inputDone = false + var outputDone = false + + while (!outputDone && coroutineContext.isActive) { + if (!inputDone) { + val inputBufferIndex = decoder.dequeueInputBuffer(10000) + if (inputBufferIndex >= 0) { + val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!! + val sampleSize = extractor.readSampleData(inputBuffer, 0) + if (sampleSize < 0) { + decoder.queueInputBuffer( + inputBufferIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + val presentationTimeUs = extractor.sampleTime + decoder.queueInputBuffer( + inputBufferIndex, + 0, + sampleSize, + presentationTimeUs, + 0, + ) + extractor.advance() + if (durationUs > 0) { + onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f)) + } + } + } + } + + val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000) + if (outputBufferIndex >= 0) { + val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!! + val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer() + while (shortBuffer.hasRemaining()) { + pcmSamples.add(shortBuffer.get() / 32768f) + } + decoder.releaseOutputBuffer(outputBufferIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + outputDone = true + } + } + } + + decoder.stop() + decoder.release() + extractor.release() + + return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration) + } + + private fun processPcmWithTarsos( + pcmData: FloatArray, + preset: VoicePreset, + sampleRate: Int, + onProgress: (Float) -> Unit, + ): FloatArray { + val factor = preset.pitchFactor + val processedSamples = mutableListOf() + val totalSamples = pcmData.size + + val wsola = + WaveformSimilarityBasedOverlapAdd( + WaveformSimilarityBasedOverlapAdd.Parameters.musicDefaults( + factor, + sampleRate.toDouble(), + ), + ) + val rateTransposer = RateTransposer(factor) + + val bufferSize = wsola.inputBufferSize + val overlap = wsola.overlap + + val tarsosDspFormat = + TarsosDSPAudioFormat( + sampleRate.toFloat(), + 16, + 1, + true, + false, + ) + + val collector = + object : AudioProcessor { + override fun process(audioEvent: AudioEvent): Boolean { + val buffer = audioEvent.floatBuffer + for (i in 0 until audioEvent.bufferSize) { + processedSamples.add(buffer[i]) + } + return true + } + + override fun processingFinished() {} + } + + val dispatcher = + AudioDispatcher( + FloatArrayAudioInputStream(pcmData, tarsosDspFormat, pcmData.size.toLong()), + bufferSize, + overlap, + ) + + wsola.setDispatcher(dispatcher) + dispatcher.addAudioProcessor(wsola) + dispatcher.addAudioProcessor(rateTransposer) + dispatcher.addAudioProcessor(collector) + + var samplesProcessed = 0 + val progressProcessor = + object : AudioProcessor { + override fun process(audioEvent: AudioEvent): Boolean { + samplesProcessed += audioEvent.bufferSize + onProgress((samplesProcessed.toFloat() / totalSamples).coerceIn(0f, 1f)) + return true + } + + override fun processingFinished() {} + } + dispatcher.addAudioProcessor(progressProcessor) + + dispatcher.run() + + return processedSamples.toFloatArray() + } + + private fun extractWaveform( + pcmData: FloatArray, + sampleRate: Int, + ): List { + val waveform = mutableListOf() + var offset = 0 + + while (offset < pcmData.size) { + val end = minOf(offset + sampleRate, pcmData.size) + var maxAmplitude = 0f + for (i in offset until end) { + val amplitude = abs(pcmData[i]) + if (amplitude > maxAmplitude) { + maxAmplitude = amplitude + } + } + waveform.add(maxAmplitude * 32768f) + offset += sampleRate + } + + return waveform + } + + private fun encodePcmToAac( + pcmData: FloatArray, + sampleRate: Int, + outputFile: File, + onProgress: (Float) -> Unit, + ) { + val format = + MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, CHANNELS) + format.setInteger( + MediaFormat.KEY_AAC_PROFILE, + MediaCodecInfo.CodecProfileLevel.AACObjectLC, + ) + format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE) + + val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC) + encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) + encoder.start() + + val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) + var audioTrackIndex = -1 + var muxerStarted = false + + val bufferInfo = MediaCodec.BufferInfo() + var inputOffset = 0 + var inputDone = false + var outputDone = false + val totalSamples = pcmData.size + + while (!outputDone) { + if (!inputDone) { + val inputBufferIndex = encoder.dequeueInputBuffer(10000) + if (inputBufferIndex >= 0) { + val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!! + inputBuffer.clear() + + val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset) + if (samplesToWrite <= 0) { + encoder.queueInputBuffer( + inputBufferIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + for (i in 0 until samplesToWrite) { + val sample = + (pcmData[inputOffset + i] * 32767) + .toInt() + .coerceIn(-32768, 32767) + .toShort() + inputBuffer.putShort(sample) + } + val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate + encoder.queueInputBuffer( + inputBufferIndex, + 0, + inputBuffer.position(), + presentationTimeUs, + 0, + ) + inputOffset += samplesToWrite + onProgress(inputOffset.toFloat() / totalSamples) + } + } + } + + val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000) + when { + outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { + audioTrackIndex = muxer.addTrack(encoder.outputFormat) + muxer.start() + muxerStarted = true + } + + outputBufferIndex >= 0 -> { + val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!! + if (muxerStarted && bufferInfo.size > 0) { + outputBuffer.position(bufferInfo.offset) + outputBuffer.limit(bufferInfo.offset + bufferInfo.size) + muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo) + } + encoder.releaseOutputBuffer(outputBufferIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + outputDone = true + } + } + } + } + + encoder.stop() + encoder.release() + muxer.stop() + muxer.release() + } +} + +private class FloatArrayAudioInputStream( + private val floatArray: FloatArray, + private val format: TarsosDSPAudioFormat, + private val frameLength: Long, +) : be.tarsos.dsp.io.TarsosDSPAudioInputStream { + private var position = 0 + + override fun getFormat(): TarsosDSPAudioFormat = format + + override fun getFrameLength(): Long = frameLength + + override fun read( + buffer: ByteArray, + offset: Int, + length: Int, + ): Int { + val converter = TarsosDSPAudioFloatConverter.getConverter(format) + val floatBuffer = FloatArray(length / 2) + val samplesToRead = minOf(floatBuffer.size, floatArray.size - position) + + if (samplesToRead <= 0) return -1 + + System.arraycopy(floatArray, position, floatBuffer, 0, samplesToRead) + position += samplesToRead + + converter.toByteArray(floatBuffer, samplesToRead, buffer, offset) + return samplesToRead * 2 + } + + override fun skip(bytesToSkip: Long): Long { + val samplesToSkip = (bytesToSkip / 2).toInt() + val actualSkip = minOf(samplesToSkip, floatArray.size - position) + position += actualSkip + return actualSkip.toLong() * 2 + } + + override fun close() {} +} From 8182268a00585228a7fa8edf77f82ca5650492ee Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 12:02:31 +0100 Subject: [PATCH 27/70] add voice anonymization state to VoiceReplyViewModel --- .../loggedIn/home/VoiceReplyViewModel.kt | 92 ++++++++++++++++++- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 3bd707f81..799fbf0a5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -34,7 +34,10 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer +import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.quartz.nip01Core.tags.people.toPTag @@ -67,6 +70,27 @@ class VoiceReplyViewModel : ViewModel() { var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null) var isUploading: Boolean by mutableStateOf(false) + var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) + var isProcessingPreset: Boolean by mutableStateOf(false) + var distortedFiles: Map by mutableStateOf(emptyMap()) + private var processingJob: Job? = null + + val activeFile: File? + get() = + if (selectedPreset == VoicePreset.NONE) { + voiceLocalFile + } else { + distortedFiles[selectedPreset]?.file + } + + val activeWaveform: List? + get() = + if (selectedPreset == VoicePreset.NONE) { + voiceRecording?.amplitudes + } else { + distortedFiles[selectedPreset]?.waveform + } + private var uploadJob: Job? = null fun init(accountVM: AccountViewModel) { @@ -113,10 +137,12 @@ class VoiceReplyViewModel : ViewModel() { fun selectRecording(recording: RecordingResult) { cancelUpload() + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file voiceMetadata = null + selectedPreset = VoicePreset.NONE } private fun cancelUpload() { @@ -135,13 +161,67 @@ class VoiceReplyViewModel : ViewModel() { Log.w("VoiceReplyViewModel", "Failed to delete voice file: ${file.absolutePath}", e) } } + + distortedFiles.values.forEach { result -> + try { + if (result.file.exists()) { + result.file.delete() + Log.d("VoiceReplyViewModel", "Deleted distorted file: ${result.file.absolutePath}") + } + } catch (e: Exception) { + Log.w("VoiceReplyViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e) + } + } + distortedFiles = emptyMap() } - fun canSend(): Boolean = voiceRecording != null && !isUploading + fun canSend(): Boolean = voiceRecording != null && !isUploading && !isProcessingPreset + + fun selectPreset(preset: VoicePreset) { + if (isProcessingPreset || preset == selectedPreset) return + + if (preset == VoicePreset.NONE) { + selectedPreset = preset + return + } + + if (distortedFiles.containsKey(preset)) { + selectedPreset = preset + return + } + + val originalFile = voiceLocalFile ?: return + + processingJob?.cancel() + processingJob = + viewModelScope.launch { + isProcessingPreset = true + try { + val anonymizer = VoiceAnonymizer() + val result = anonymizer.anonymize(originalFile, preset) + + result + .onSuccess { anonymizedResult -> + distortedFiles = distortedFiles + (preset to anonymizedResult) + selectedPreset = preset + }.onFailure { error -> + Log.w("VoiceReplyViewModel", "Failed to anonymize voice", error) + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + } + } finally { + isProcessingPreset = false + processingJob = null + } + } + } fun sendVoiceReply(onSuccess: () -> Unit) { val note = replyToNote ?: return val recording = voiceRecording ?: return + val fileToUpload = activeFile ?: recording.file val serverToUse = voiceSelectedServer ?: accountViewModel.account.settings.defaultFileServer cancelUpload() @@ -154,7 +234,7 @@ class VoiceReplyViewModel : ViewModel() { try { val result = withContext(Dispatchers.IO) { - val uri = android.net.Uri.fromFile(recording.file) + val uri = android.net.Uri.fromFile(fileToUpload) orchestrator.upload( uri = uri, mimeType = recording.mimeType, @@ -168,7 +248,7 @@ class VoiceReplyViewModel : ViewModel() { ) } - handleUploadResult(note, recording, serverToUse, result, onSuccess) + handleUploadResult(note, recording, activeWaveform ?: recording.amplitudes, serverToUse, result, onSuccess) } catch (e: CancellationException) { Log.w("VoiceReplyViewModel", "User canceled, or ViewModel cleared", e) } catch (e: Exception) { @@ -192,6 +272,7 @@ class VoiceReplyViewModel : ViewModel() { private suspend fun handleUploadResult( note: Note, recording: RecordingResult, + waveform: List, server: ServerName, result: UploadingState, onSuccess: () -> Unit, @@ -211,7 +292,7 @@ class VoiceReplyViewModel : ViewModel() { mimeType = recording.mimeType, hash = orchestratorResult.fileHeader.hash, duration = recording.duration, - waveform = recording.amplitudes, + waveform = waveform, ) // Check if replying to a voice event @@ -266,6 +347,7 @@ class VoiceReplyViewModel : ViewModel() { fun cancel() { cancelUpload() + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -273,6 +355,8 @@ class VoiceReplyViewModel : ViewModel() { voiceSelectedServer = null isUploading = false voiceOrchestrator = null + selectedPreset = VoicePreset.NONE + isProcessingPreset = false } override fun onCleared() { From 63d7ba07b95353c6fb23676fd99062e22a0a5e55 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 15:11:33 +0100 Subject: [PATCH 28/70] add voice preset selector UI to VoiceReplyScreen --- .../ui/actions/uploads/VoicePresetSelector.kt | 80 +++++++++++++++++++ .../screen/loggedIn/home/VoiceReplyScreen.kt | 17 +++- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt new file mode 100644 index 000000000..aae4818af --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.uploads + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.ui.stringRes + +@Composable +fun VoicePresetSelector( + selectedPreset: VoicePreset, + isProcessing: Boolean, + onPresetSelected: (VoicePreset) -> Unit, + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + ) { + VoicePreset.entries.forEach { preset -> + val isSelected = preset == selectedPreset + val isThisProcessing = isProcessing && preset == selectedPreset + val isEnabled = !isProcessing || preset == VoicePreset.NONE + + FilterChip( + selected = isSelected, + onClick = { if (isEnabled) onPresetSelected(preset) }, + enabled = isEnabled, + label = { + if (isThisProcessing) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary, + ) + } else { + Text(stringRes(context, preset.labelRes)) + } + }, + colors = + FilterChipDefaults.filterChipColors( + selectedContainerColor = MaterialTheme.colorScheme.primary, + selectedLabelColor = MaterialTheme.colorScheme.onPrimary, + ), + ) + } + } +} 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 15f30bb19..3794060bc 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 @@ -54,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.VoicePresetSelector import com.vitorpamplona.amethyst.ui.actions.uploads.formatSecondsToTime import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar @@ -153,15 +154,27 @@ private fun VoiceReplyScreenBody( UploadProgressIndicator(orchestrator) } ?: run { viewModel.getVoicePreviewMetadata()?.let { metadata -> + val displayMetadata = + metadata.copy( + waveform = viewModel.activeWaveform ?: metadata.waveform, + ) VoiceMessagePreview( - voiceMetadata = metadata, - localFile = viewModel.voiceLocalFile, + voiceMetadata = displayMetadata, + localFile = viewModel.activeFile, onRemove = { viewModel.cancel() nav.popBack() }, ) } + + // Voice preset selector (only show when not uploading) + Spacer(modifier = Modifier.height(12.dp)) + VoicePresetSelector( + selectedPreset = viewModel.selectedPreset, + isProcessing = viewModel.isProcessingPreset, + onPresetSelected = { viewModel.selectPreset(it) }, + ) } Spacer(modifier = Modifier.height(16.dp)) From 9c0fbbf8a40b6ff21db286bd491026b7edd70456 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 15:54:15 +0100 Subject: [PATCH 29/70] correct pitch selection Add to voice note screen (new note) --- .../ui/actions/uploads/VoicePreset.kt | 6 +- .../loggedIn/home/ShortNotePostScreen.kt | 19 +++- .../loggedIn/home/ShortNotePostViewModel.kt | 99 ++++++++++++++++++- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt index 1d16a3c8e..176baab62 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt @@ -29,7 +29,7 @@ enum class VoicePreset( @StringRes val labelRes: Int, ) { NONE(1.0, 0.0, R.string.voice_preset_none), - DEEP(0.75, -3.0, R.string.voice_preset_deep), - HIGH(1.4, 4.0, R.string.voice_preset_high), - NEUTRAL(1.0, -2.0, R.string.voice_preset_neutral), + DEEP(1.4, -3.0, R.string.voice_preset_deep), + HIGH(0.75, 4.0, R.string.voice_preset_high), + NEUTRAL(0.9, -2.0, R.string.voice_preset_neutral), } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 504c723be..325d4b3e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview +import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar @@ -366,11 +367,25 @@ private fun NewPostScreenBody( postViewModel.voiceOrchestrator?.let { orchestrator -> UploadProgressIndicator(orchestrator) } ?: run { + val displayMetadata = + metadata.copy( + waveform = postViewModel.activeWaveform ?: metadata.waveform, + ) VoiceMessagePreview( - voiceMetadata = metadata, - localFile = postViewModel.voiceLocalFile, + voiceMetadata = displayMetadata, + localFile = postViewModel.activeFile, onRemove = { postViewModel.removeVoiceMessage() }, ) + + // Voice preset selector (only show when not uploading and voice is pending) + if (postViewModel.voiceRecording != null) { + Spacer(modifier = Modifier.height(12.dp)) + VoicePresetSelector( + selectedPreset = postViewModel.selectedPreset, + isProcessing = postViewModel.isProcessingPreset, + onPresetSelected = { postViewModel.selectPreset(it) }, + ) + } } FileServerSelectionRow( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index d810088ed..9f4e5c237 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -50,9 +50,12 @@ import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer +import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber @@ -128,6 +131,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -194,6 +198,28 @@ open class ShortNotePostViewModel : var voiceSelectedServer by mutableStateOf(null) var voiceOrchestrator by mutableStateOf(null) + // Voice Anonymization + var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) + var isProcessingPreset: Boolean by mutableStateOf(false) + var distortedFiles: Map by mutableStateOf(emptyMap()) + private var processingJob: Job? = null + + val activeFile: java.io.File? + get() = + if (selectedPreset == VoicePreset.NONE) { + voiceLocalFile + } else { + distortedFiles[selectedPreset]?.file + } + + val activeWaveform: List? + get() = + if (selectedPreset == VoicePreset.NONE) { + voiceRecording?.amplitudes + } else { + distortedFiles[selectedPreset]?.waveform + } + // Polls var canUsePoll by mutableStateOf(false) var wantsPoll by mutableStateOf(false) @@ -794,6 +820,7 @@ open class ShortNotePostViewModel : multiOrchestrator = null isUploadingImage = false + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -801,6 +828,8 @@ open class ShortNotePostViewModel : voiceMetadata = null voiceSelectedServer = null voiceOrchestrator = null + selectedPreset = VoicePreset.NONE + isProcessingPreset = false pTags = null wantsPoll = false @@ -922,7 +951,7 @@ open class ShortNotePostViewModel : fun canPost(): Boolean { // Voice messages can be posted without text (with either uploaded or pending recording) if (voiceMetadata != null || voiceRecording != null) { - return !isUploadingVoice && !isUploadingImage + return !isUploadingVoice && !isUploadingImage && !isProcessingPreset } // Regular text/media posts require text @@ -951,10 +980,14 @@ open class ShortNotePostViewModel : } fun selectVoiceRecording(recording: RecordingResult) { - // Delete any existing temp file before replacing + // Cancel any ongoing processing and delete existing files + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file + voiceMetadata = null + selectedPreset = VoicePreset.NONE + isProcessingPreset = false } fun getVoicePreviewMetadata(): AudioMeta? = @@ -967,7 +1000,49 @@ open class ShortNotePostViewModel : ) } + fun selectPreset(preset: VoicePreset) { + if (isProcessingPreset || preset == selectedPreset) return + + if (preset == VoicePreset.NONE) { + selectedPreset = preset + return + } + + if (distortedFiles.containsKey(preset)) { + selectedPreset = preset + return + } + + val originalFile = voiceLocalFile ?: return + + processingJob?.cancel() + processingJob = + viewModelScope.launch { + isProcessingPreset = true + try { + val anonymizer = VoiceAnonymizer() + val result = anonymizer.anonymize(originalFile, preset) + + result + .onSuccess { anonymizedResult -> + distortedFiles = distortedFiles + (preset to anonymizedResult) + selectedPreset = preset + }.onFailure { error -> + Log.w("ShortNotePostViewModel", "Failed to anonymize voice", error) + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + } + } finally { + isProcessingPreset = false + processingJob = null + } + } + } + fun removeVoiceMessage() { + processingJob?.cancel() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -975,6 +1050,8 @@ open class ShortNotePostViewModel : voiceSelectedServer = null isUploadingVoice = false voiceOrchestrator = null + selectedPreset = VoicePreset.NONE + isProcessingPreset = false } private fun deleteVoiceLocalFile() { @@ -988,6 +1065,18 @@ open class ShortNotePostViewModel : Log.w("ShortNotePostViewModel", "Failed to delete voice file: ${file.absolutePath}", e) } } + + distortedFiles.values.forEach { result -> + try { + if (result.file.exists()) { + result.file.delete() + Log.d("ShortNotePostViewModel", "Deleted distorted file: ${result.file.absolutePath}") + } + } catch (e: Exception) { + Log.w("ShortNotePostViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e) + } + } + distortedFiles = emptyMap() } suspend fun uploadVoiceMessageSync( @@ -995,6 +1084,8 @@ open class ShortNotePostViewModel : onError: (title: String, message: String) -> Unit, ) { val recording = voiceRecording ?: return + val fileToUpload = activeFile ?: recording.file + val waveform = activeWaveform ?: recording.amplitudes val appContext = Amethyst.instance.appContext val uploadErrorTitle = stringRes(appContext, R.string.upload_error_title) val uploadVoiceNip95NotSupported = stringRes(appContext, R.string.upload_error_voice_message_nip95_not_supported) @@ -1006,7 +1097,7 @@ open class ShortNotePostViewModel : isUploadingVoice = true try { - val uri = android.net.Uri.fromFile(recording.file) + val uri = android.net.Uri.fromFile(fileToUpload) val orchestrator = UploadOrchestrator() voiceOrchestrator = orchestrator @@ -1033,7 +1124,7 @@ open class ShortNotePostViewModel : mimeType = recording.mimeType, hash = orchestratorResult.fileHeader.hash, duration = recording.duration, - waveform = recording.amplitudes, + waveform = waveform, ) // Delete the local file after successful upload deleteVoiceLocalFile() From 6f8533f5e1f5435cb4e6cf58b50b410f6e1375bc Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 21:51:22 +0100 Subject: [PATCH 30/70] added explainer to UI --- .../loggedIn/home/ShortNotePostScreen.kt | 29 ++++++++++++++++++- .../screen/loggedIn/home/VoiceReplyScreen.kt | 28 +++++++++++++++++- amethyst/src/main/res/values/strings.xml | 2 ++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 325d4b3e1..029fdc454 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -25,6 +25,7 @@ import android.net.Uri import android.os.Parcelable import androidx.activity.compose.BackHandler import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -38,20 +39,24 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel @@ -96,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp @@ -377,9 +383,30 @@ private fun NewPostScreenBody( onRemove = { postViewModel.removeVoiceMessage() }, ) - // Voice preset selector (only show when not uploading and voice is pending) + // Voice anonymization section (only show when not uploading and voice is pending) if (postViewModel.voiceRecording != null) { + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(thickness = DividerThickness) Spacer(modifier = Modifier.height(12.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.voice_anonymize_title), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.voice_anonymize_description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) VoicePresetSelector( selectedPreset = postViewModel.selectedPreset, isProcessing = postViewModel.isProcessingPreset, 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 3794060bc..a6d7b888a 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 @@ -37,6 +37,7 @@ 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.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -47,6 +48,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R @@ -61,7 +64,9 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -168,8 +173,29 @@ private fun VoiceReplyScreenBody( ) } - // Voice preset selector (only show when not uploading) + // Voice anonymization section + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(thickness = DividerThickness) Spacer(modifier = Modifier.height(12.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.voice_anonymize_title), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.voice_anonymize_description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) VoicePresetSelector( selectedPreset = viewModel.selectedPreset, isProcessing = viewModel.isProcessingPreset, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index ebec66ae8..a78c01f31 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -188,6 +188,8 @@ Deep High Neutral + Anonymize + Alters your voice pitch. Note: basic pitch changes can potentially be reversed by determined listeners. User does not have a lightning address set up to receive sats "reply here.. " Copies the Note ID to the clipboard for sharing in Nostr From a4af706ebd9ad3f291d086fa785954069400385f Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 21:58:40 +0100 Subject: [PATCH 31/70] correct the item where progress spinner happens on --- .../ui/actions/uploads/VoicePresetSelector.kt | 5 +++-- .../screen/loggedIn/home/ShortNotePostScreen.kt | 2 +- .../loggedIn/home/ShortNotePostViewModel.kt | 16 ++++++++-------- .../ui/screen/loggedIn/home/VoiceReplyScreen.kt | 2 +- .../screen/loggedIn/home/VoiceReplyViewModel.kt | 13 +++++++------ 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt index aae4818af..44da46683 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePresetSelector.kt @@ -39,11 +39,12 @@ import com.vitorpamplona.amethyst.ui.stringRes @Composable fun VoicePresetSelector( selectedPreset: VoicePreset, - isProcessing: Boolean, + processingPreset: VoicePreset?, onPresetSelected: (VoicePreset) -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current + val isProcessing = processingPreset != null Row( modifier = modifier.fillMaxWidth(), @@ -51,7 +52,7 @@ fun VoicePresetSelector( ) { VoicePreset.entries.forEach { preset -> val isSelected = preset == selectedPreset - val isThisProcessing = isProcessing && preset == selectedPreset + val isThisProcessing = preset == processingPreset val isEnabled = !isProcessing || preset == VoicePreset.NONE FilterChip( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 029fdc454..88ac4f5a4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -409,7 +409,7 @@ private fun NewPostScreenBody( Spacer(modifier = Modifier.height(8.dp)) VoicePresetSelector( selectedPreset = postViewModel.selectedPreset, - isProcessing = postViewModel.isProcessingPreset, + processingPreset = postViewModel.processingPreset, onPresetSelected = { postViewModel.selectPreset(it) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 9f4e5c237..cd0030f0a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -200,7 +200,7 @@ open class ShortNotePostViewModel : // Voice Anonymization var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) - var isProcessingPreset: Boolean by mutableStateOf(false) + var processingPreset: VoicePreset? by mutableStateOf(null) var distortedFiles: Map by mutableStateOf(emptyMap()) private var processingJob: Job? = null @@ -829,7 +829,7 @@ open class ShortNotePostViewModel : voiceSelectedServer = null voiceOrchestrator = null selectedPreset = VoicePreset.NONE - isProcessingPreset = false + processingPreset = null pTags = null wantsPoll = false @@ -951,7 +951,7 @@ open class ShortNotePostViewModel : fun canPost(): Boolean { // Voice messages can be posted without text (with either uploaded or pending recording) if (voiceMetadata != null || voiceRecording != null) { - return !isUploadingVoice && !isUploadingImage && !isProcessingPreset + return !isUploadingVoice && !isUploadingImage && processingPreset == null } // Regular text/media posts require text @@ -987,7 +987,7 @@ open class ShortNotePostViewModel : voiceLocalFile = recording.file voiceMetadata = null selectedPreset = VoicePreset.NONE - isProcessingPreset = false + processingPreset = null } fun getVoicePreviewMetadata(): AudioMeta? = @@ -1001,7 +1001,7 @@ open class ShortNotePostViewModel : } fun selectPreset(preset: VoicePreset) { - if (isProcessingPreset || preset == selectedPreset) return + if (processingPreset != null || preset == selectedPreset) return if (preset == VoicePreset.NONE) { selectedPreset = preset @@ -1018,7 +1018,7 @@ open class ShortNotePostViewModel : processingJob?.cancel() processingJob = viewModelScope.launch { - isProcessingPreset = true + processingPreset = preset try { val anonymizer = VoiceAnonymizer() val result = anonymizer.anonymize(originalFile, preset) @@ -1035,7 +1035,7 @@ open class ShortNotePostViewModel : ) } } finally { - isProcessingPreset = false + processingPreset = null processingJob = null } } @@ -1051,7 +1051,7 @@ open class ShortNotePostViewModel : isUploadingVoice = false voiceOrchestrator = null selectedPreset = VoicePreset.NONE - isProcessingPreset = false + processingPreset = null } private fun deleteVoiceLocalFile() { 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 a6d7b888a..f1dae3aa5 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 @@ -198,7 +198,7 @@ private fun VoiceReplyScreenBody( Spacer(modifier = Modifier.height(8.dp)) VoicePresetSelector( selectedPreset = viewModel.selectedPreset, - isProcessing = viewModel.isProcessingPreset, + processingPreset = viewModel.processingPreset, onPresetSelected = { viewModel.selectPreset(it) }, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 799fbf0a5..1c99db810 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -71,7 +71,7 @@ class VoiceReplyViewModel : ViewModel() { var isUploading: Boolean by mutableStateOf(false) var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) - var isProcessingPreset: Boolean by mutableStateOf(false) + var processingPreset: VoicePreset? by mutableStateOf(null) var distortedFiles: Map by mutableStateOf(emptyMap()) private var processingJob: Job? = null @@ -138,6 +138,7 @@ class VoiceReplyViewModel : ViewModel() { fun selectRecording(recording: RecordingResult) { cancelUpload() processingJob?.cancel() + processingPreset = null deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file @@ -175,10 +176,10 @@ class VoiceReplyViewModel : ViewModel() { distortedFiles = emptyMap() } - fun canSend(): Boolean = voiceRecording != null && !isUploading && !isProcessingPreset + fun canSend(): Boolean = voiceRecording != null && !isUploading && processingPreset == null fun selectPreset(preset: VoicePreset) { - if (isProcessingPreset || preset == selectedPreset) return + if (processingPreset != null || preset == selectedPreset) return if (preset == VoicePreset.NONE) { selectedPreset = preset @@ -195,7 +196,7 @@ class VoiceReplyViewModel : ViewModel() { processingJob?.cancel() processingJob = viewModelScope.launch { - isProcessingPreset = true + processingPreset = preset try { val anonymizer = VoiceAnonymizer() val result = anonymizer.anonymize(originalFile, preset) @@ -212,7 +213,7 @@ class VoiceReplyViewModel : ViewModel() { ) } } finally { - isProcessingPreset = false + processingPreset = null processingJob = null } } @@ -356,7 +357,7 @@ class VoiceReplyViewModel : ViewModel() { isUploading = false voiceOrchestrator = null selectedPreset = VoicePreset.NONE - isProcessingPreset = false + processingPreset = null } override fun onCleared() { From 666015b8eb5b9050a42acdcc3b3959ff437789ea Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 22:27:00 +0100 Subject: [PATCH 32/70] reduce duplication: New shared controller for state + processing + distorted file cleanup New shared UI section for anonymization header + preset selector --- .../uploads/VoiceAnonymizationController.kt | 126 ++++++++++++++++++ .../uploads/VoiceAnonymizationSection.kt | 79 +++++++++++ .../ui/actions/uploads/VoiceAnonymizer.kt | 4 +- .../ui/actions/uploads/VoicePreset.kt | 12 +- .../loggedIn/home/ShortNotePostScreen.kt | 32 +---- .../loggedIn/home/ShortNotePostViewModel.kt | 103 ++++---------- .../screen/loggedIn/home/VoiceReplyScreen.kt | 31 +---- .../loggedIn/home/VoiceReplyViewModel.kt | 98 ++++---------- 8 files changed, 265 insertions(+), 220 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt new file mode 100644 index 000000000..b720e20d8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.uploads + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.vitorpamplona.quartz.utils.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import java.io.File + +class VoiceAnonymizationController( + private val scope: CoroutineScope, + private val logTag: String, + private val onError: (Throwable) -> Unit, +) { + var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) + private set + var processingPreset: VoicePreset? by mutableStateOf(null) + private set + var distortedFiles: Map by mutableStateOf(emptyMap()) + private set + + private var processingJob: Job? = null + + fun activeFile(originalFile: File?): File? = + if (selectedPreset == VoicePreset.NONE) { + originalFile + } else { + distortedFiles[selectedPreset]?.file + } + + fun activeWaveform(originalWaveform: List?): List? = + if (selectedPreset == VoicePreset.NONE) { + originalWaveform + } else { + distortedFiles[selectedPreset]?.waveform + } + + fun selectPreset( + preset: VoicePreset, + originalFile: File?, + ) { + if (processingPreset != null || preset == selectedPreset) return + + if (preset == VoicePreset.NONE) { + selectedPreset = preset + return + } + + if (distortedFiles.containsKey(preset)) { + selectedPreset = preset + return + } + + val file = originalFile ?: return + + processingJob?.cancel() + processingJob = + scope.launch { + processingPreset = preset + try { + val anonymizer = VoiceAnonymizer() + val result = anonymizer.anonymize(file, preset) + + result + .onSuccess { anonymizedResult -> + distortedFiles = distortedFiles + (preset to anonymizedResult) + selectedPreset = preset + }.onFailure { error -> + Log.w(logTag, "Failed to anonymize voice", error) + onError(error) + } + } finally { + processingPreset = null + processingJob = null + } + } + } + + fun clear() { + cancelProcessing() + deleteDistortedFiles() + selectedPreset = VoicePreset.NONE + } + + fun deleteDistortedFiles() { + distortedFiles.values.forEach { result -> + try { + if (result.file.exists()) { + result.file.delete() + Log.d(logTag, "Deleted distorted file: ${result.file.absolutePath}") + } + } catch (e: Exception) { + Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}", e) + } + } + distortedFiles = emptyMap() + } + + private fun cancelProcessing() { + processingJob?.cancel() + processingJob = null + processingPreset = null + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt new file mode 100644 index 000000000..3b7f02028 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.actions.uploads + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.amethyst.ui.theme.Size5dp + +@Composable +fun VoiceAnonymizationSection( + selectedPreset: VoicePreset, + processingPreset: VoicePreset?, + onPresetSelected: (VoicePreset) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + ) { + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider(thickness = DividerThickness) + Spacer(modifier = Modifier.height(12.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(Size5dp), + ) { + Text( + text = stringRes(R.string.voice_anonymize_title), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + text = stringRes(R.string.voice_anonymize_description), + style = MaterialTheme.typography.bodySmall, + color = Color.Gray, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + VoicePresetSelector( + selectedPreset = selectedPreset, + processingPreset = processingPreset, + onPresetSelected = onPresetSelected, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 08d15c859..1359f3318 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -34,11 +34,11 @@ import be.tarsos.dsp.io.TarsosDSPAudioFloatConverter import be.tarsos.dsp.io.TarsosDSPAudioFormat import be.tarsos.dsp.resample.RateTransposer import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext import java.io.File import java.nio.ByteOrder -import kotlin.coroutines.coroutineContext import kotlin.math.abs data class AnonymizedResult( @@ -147,7 +147,7 @@ class VoiceAnonymizer { var inputDone = false var outputDone = false - while (!outputDone && coroutineContext.isActive) { + while (!outputDone && currentCoroutineContext().isActive) { if (!inputDone) { val inputBufferIndex = decoder.dequeueInputBuffer(10000) if (inputBufferIndex >= 0) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt index 176baab62..49544e51a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt @@ -20,16 +20,14 @@ */ package com.vitorpamplona.amethyst.ui.actions.uploads -import androidx.annotation.StringRes import com.vitorpamplona.amethyst.R enum class VoicePreset( val pitchFactor: Double, - val formantShift: Double, - @StringRes val labelRes: Int, + val labelRes: Int, ) { - NONE(1.0, 0.0, R.string.voice_preset_none), - DEEP(1.4, -3.0, R.string.voice_preset_deep), - HIGH(0.75, 4.0, R.string.voice_preset_high), - NEUTRAL(0.9, -2.0, R.string.voice_preset_neutral), + NONE(1.0, R.string.voice_preset_none), + DEEP(1.4, R.string.voice_preset_deep), + HIGH(0.75, R.string.voice_preset_high), + NEUTRAL(0.9, R.string.voice_preset_neutral), } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 88ac4f5a4..65a2b051d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -25,7 +25,6 @@ import android.net.Uri import android.os.Parcelable import androidx.activity.compose.BackHandler import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -39,24 +38,20 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Switch -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel @@ -70,8 +65,8 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.navigation.navs.Nav import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar @@ -101,7 +96,6 @@ import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp @@ -385,29 +379,7 @@ private fun NewPostScreenBody( // Voice anonymization section (only show when not uploading and voice is pending) if (postViewModel.voiceRecording != null) { - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider(thickness = DividerThickness) - Spacer(modifier = Modifier.height(12.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(Size5dp), - ) { - Text( - text = stringRes(R.string.voice_anonymize_title), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(R.string.voice_anonymize_description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - VoicePresetSelector( + VoiceAnonymizationSection( selectedPreset = postViewModel.selectedPreset, processingPreset = postViewModel.processingPreset, onPresetSelected = { postViewModel.selectPreset(it) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index cd0030f0a..538057970 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -50,11 +50,10 @@ import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType -import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMediaProcessing -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.note.creators.draftTags.DraftTagState import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.EmojiSuggestionState @@ -131,7 +130,6 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent import com.vitorpamplona.quartz.utils.Log import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -199,26 +197,29 @@ open class ShortNotePostViewModel : var voiceOrchestrator by mutableStateOf(null) // Voice Anonymization - var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) - var processingPreset: VoicePreset? by mutableStateOf(null) - var distortedFiles: Map by mutableStateOf(emptyMap()) - private var processingJob: Job? = null + private val voiceAnonymization = + VoiceAnonymizationController( + scope = viewModelScope, + logTag = "ShortNotePostViewModel", + onError = { error -> + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + }, + ) val activeFile: java.io.File? - get() = - if (selectedPreset == VoicePreset.NONE) { - voiceLocalFile - } else { - distortedFiles[selectedPreset]?.file - } + get() = voiceAnonymization.activeFile(voiceLocalFile) val activeWaveform: List? - get() = - if (selectedPreset == VoicePreset.NONE) { - voiceRecording?.amplitudes - } else { - distortedFiles[selectedPreset]?.waveform - } + get() = voiceAnonymization.activeWaveform(voiceRecording?.amplitudes) + + val selectedPreset: VoicePreset + get() = voiceAnonymization.selectedPreset + + val processingPreset: VoicePreset? + get() = voiceAnonymization.processingPreset // Polls var canUsePoll by mutableStateOf(false) @@ -820,7 +821,7 @@ open class ShortNotePostViewModel : multiOrchestrator = null isUploadingImage = false - processingJob?.cancel() + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -828,8 +829,6 @@ open class ShortNotePostViewModel : voiceMetadata = null voiceSelectedServer = null voiceOrchestrator = null - selectedPreset = VoicePreset.NONE - processingPreset = null pTags = null wantsPoll = false @@ -981,13 +980,11 @@ open class ShortNotePostViewModel : fun selectVoiceRecording(recording: RecordingResult) { // Cancel any ongoing processing and delete existing files - processingJob?.cancel() + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file voiceMetadata = null - selectedPreset = VoicePreset.NONE - processingPreset = null } fun getVoicePreviewMetadata(): AudioMeta? = @@ -1001,48 +998,11 @@ open class ShortNotePostViewModel : } fun selectPreset(preset: VoicePreset) { - if (processingPreset != null || preset == selectedPreset) return - - if (preset == VoicePreset.NONE) { - selectedPreset = preset - return - } - - if (distortedFiles.containsKey(preset)) { - selectedPreset = preset - return - } - - val originalFile = voiceLocalFile ?: return - - processingJob?.cancel() - processingJob = - viewModelScope.launch { - processingPreset = preset - try { - val anonymizer = VoiceAnonymizer() - val result = anonymizer.anonymize(originalFile, preset) - - result - .onSuccess { anonymizedResult -> - distortedFiles = distortedFiles + (preset to anonymizedResult) - selectedPreset = preset - }.onFailure { error -> - Log.w("ShortNotePostViewModel", "Failed to anonymize voice", error) - accountViewModel.toastManager.toast( - stringRes(Amethyst.instance.appContext, R.string.error), - error.message ?: "Voice anonymization failed", - ) - } - } finally { - processingPreset = null - processingJob = null - } - } + voiceAnonymization.selectPreset(preset, voiceLocalFile) } fun removeVoiceMessage() { - processingJob?.cancel() + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -1050,8 +1010,6 @@ open class ShortNotePostViewModel : voiceSelectedServer = null isUploadingVoice = false voiceOrchestrator = null - selectedPreset = VoicePreset.NONE - processingPreset = null } private fun deleteVoiceLocalFile() { @@ -1065,18 +1023,6 @@ open class ShortNotePostViewModel : Log.w("ShortNotePostViewModel", "Failed to delete voice file: ${file.absolutePath}", e) } } - - distortedFiles.values.forEach { result -> - try { - if (result.file.exists()) { - result.file.delete() - Log.d("ShortNotePostViewModel", "Deleted distorted file: ${result.file.absolutePath}") - } - } catch (e: Exception) { - Log.w("ShortNotePostViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e) - } - } - distortedFiles = emptyMap() } suspend fun uploadVoiceMessageSync( @@ -1128,6 +1074,7 @@ open class ShortNotePostViewModel : ) // Delete the local file after successful upload deleteVoiceLocalFile() + voiceAnonymization.deleteDistortedFiles() voiceLocalFile = null voiceRecording = null } 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 f1dae3aa5..a2573663b 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 @@ -37,7 +37,6 @@ 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.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -48,25 +47,21 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.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.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview -import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePresetSelector 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 import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -174,29 +169,7 @@ private fun VoiceReplyScreenBody( } // Voice anonymization section - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider(thickness = DividerThickness) - Spacer(modifier = Modifier.height(12.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(Size5dp), - ) { - Text( - text = stringRes(R.string.voice_anonymize_title), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(R.string.voice_anonymize_description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 3, - overflow = TextOverflow.Ellipsis, - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - VoicePresetSelector( + VoiceAnonymizationSection( selectedPreset = viewModel.selectedPreset, processingPreset = viewModel.processingPreset, onPresetSelected = { viewModel.selectPreset(it) }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 1c99db810..eea2a0af6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -34,9 +34,8 @@ import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.service.uploads.UploadingState import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType -import com.vitorpamplona.amethyst.ui.actions.uploads.AnonymizedResult import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult -import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizer +import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController import com.vitorpamplona.amethyst.ui.actions.uploads.VoicePreset import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes @@ -70,26 +69,29 @@ class VoiceReplyViewModel : ViewModel() { var voiceOrchestrator: UploadOrchestrator? by mutableStateOf(null) var isUploading: Boolean by mutableStateOf(false) - var selectedPreset: VoicePreset by mutableStateOf(VoicePreset.NONE) - var processingPreset: VoicePreset? by mutableStateOf(null) - var distortedFiles: Map by mutableStateOf(emptyMap()) - private var processingJob: Job? = null + private val voiceAnonymization = + VoiceAnonymizationController( + scope = viewModelScope, + logTag = "VoiceReplyViewModel", + onError = { error -> + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + }, + ) val activeFile: File? - get() = - if (selectedPreset == VoicePreset.NONE) { - voiceLocalFile - } else { - distortedFiles[selectedPreset]?.file - } + get() = voiceAnonymization.activeFile(voiceLocalFile) val activeWaveform: List? - get() = - if (selectedPreset == VoicePreset.NONE) { - voiceRecording?.amplitudes - } else { - distortedFiles[selectedPreset]?.waveform - } + get() = voiceAnonymization.activeWaveform(voiceRecording?.amplitudes) + + val selectedPreset: VoicePreset + get() = voiceAnonymization.selectedPreset + + val processingPreset: VoicePreset? + get() = voiceAnonymization.processingPreset private var uploadJob: Job? = null @@ -137,13 +139,11 @@ class VoiceReplyViewModel : ViewModel() { fun selectRecording(recording: RecordingResult) { cancelUpload() - processingJob?.cancel() - processingPreset = null + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = recording voiceLocalFile = recording.file voiceMetadata = null - selectedPreset = VoicePreset.NONE } private fun cancelUpload() { @@ -162,61 +162,12 @@ class VoiceReplyViewModel : ViewModel() { Log.w("VoiceReplyViewModel", "Failed to delete voice file: ${file.absolutePath}", e) } } - - distortedFiles.values.forEach { result -> - try { - if (result.file.exists()) { - result.file.delete() - Log.d("VoiceReplyViewModel", "Deleted distorted file: ${result.file.absolutePath}") - } - } catch (e: Exception) { - Log.w("VoiceReplyViewModel", "Failed to delete distorted file: ${result.file.absolutePath}", e) - } - } - distortedFiles = emptyMap() } fun canSend(): Boolean = voiceRecording != null && !isUploading && processingPreset == null fun selectPreset(preset: VoicePreset) { - if (processingPreset != null || preset == selectedPreset) return - - if (preset == VoicePreset.NONE) { - selectedPreset = preset - return - } - - if (distortedFiles.containsKey(preset)) { - selectedPreset = preset - return - } - - val originalFile = voiceLocalFile ?: return - - processingJob?.cancel() - processingJob = - viewModelScope.launch { - processingPreset = preset - try { - val anonymizer = VoiceAnonymizer() - val result = anonymizer.anonymize(originalFile, preset) - - result - .onSuccess { anonymizedResult -> - distortedFiles = distortedFiles + (preset to anonymizedResult) - selectedPreset = preset - }.onFailure { error -> - Log.w("VoiceReplyViewModel", "Failed to anonymize voice", error) - accountViewModel.toastManager.toast( - stringRes(Amethyst.instance.appContext, R.string.error), - error.message ?: "Voice anonymization failed", - ) - } - } finally { - processingPreset = null - processingJob = null - } - } + voiceAnonymization.selectPreset(preset, voiceLocalFile) } fun sendVoiceReply(onSuccess: () -> Unit) { @@ -326,6 +277,7 @@ class VoiceReplyViewModel : ViewModel() { } deleteVoiceLocalFile() + voiceAnonymization.deleteDistortedFiles() voiceLocalFile = null voiceRecording = null voiceMetadata = audioMeta @@ -348,7 +300,7 @@ class VoiceReplyViewModel : ViewModel() { fun cancel() { cancelUpload() - processingJob?.cancel() + voiceAnonymization.clear() deleteVoiceLocalFile() voiceRecording = null voiceLocalFile = null @@ -356,8 +308,6 @@ class VoiceReplyViewModel : ViewModel() { voiceSelectedServer = null isUploading = false voiceOrchestrator = null - selectedPreset = VoicePreset.NONE - processingPreset = null } override fun onCleared() { From 3287e3c031dd399f34ef3a16ee72393608524286 Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 22:46:03 +0100 Subject: [PATCH 33/70] =?UTF-8?q?add=20a=20=C2=B110%=20random=20variation?= =?UTF-8?q?=20to=20HIGH=20and=20DEEP=20pitch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../amethyst/ui/actions/uploads/VoiceAnonymizer.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 1359f3318..ee036c482 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -50,7 +50,6 @@ data class AnonymizedResult( class VoiceAnonymizer { companion object { private const val TAG = "VoiceAnonymizer" - private const val SAMPLE_RATE = 44100 private const val CHANNELS = 1 private const val BIT_RATE = 128000 } @@ -206,7 +205,16 @@ class VoiceAnonymizer { sampleRate: Int, onProgress: (Float) -> Unit, ): FloatArray { - val factor = preset.pitchFactor + val baseFactor = preset.pitchFactor + val factor = + when (preset) { + VoicePreset.DEEP, VoicePreset.HIGH -> { + // Add ±10% random variation + val randomShift = 0.9 + (Math.random() * 0.2) + baseFactor * randomShift + } + else -> baseFactor + } val processedSamples = mutableListOf() val totalSamples = pcmData.size From f13a9164c2d931ad9b8998e2b3e944cf37d0e88b Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 22:55:53 +0100 Subject: [PATCH 34/70] code review: resource leaks and init guard --- .../ui/actions/uploads/VoiceAnonymizer.kt | 289 +++++++++--------- .../loggedIn/home/VoiceReplyViewModel.kt | 10 +- 2 files changed, 154 insertions(+), 145 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index ee036c482..391b79349 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -112,91 +112,94 @@ class VoiceAnonymizer { onProgress: (Float) -> Unit, ): DecodedAudio { val extractor = MediaExtractor() - extractor.setDataSource(inputFile.absolutePath) + var decoder: MediaCodec? = null - var audioTrackIndex = -1 - var format: MediaFormat? = null - for (i in 0 until extractor.trackCount) { - val trackFormat = extractor.getTrackFormat(i) - val mime = trackFormat.getString(MediaFormat.KEY_MIME) - if (mime?.startsWith("audio/") == true) { - audioTrackIndex = i - format = trackFormat - break + try { + extractor.setDataSource(inputFile.absolutePath) + + var audioTrackIndex = -1 + var format: MediaFormat? = null + for (i in 0 until extractor.trackCount) { + val trackFormat = extractor.getTrackFormat(i) + val mime = trackFormat.getString(MediaFormat.KEY_MIME) + if (mime?.startsWith("audio/") == true) { + audioTrackIndex = i + format = trackFormat + break + } } - } - if (audioTrackIndex == -1 || format == null) { - extractor.release() - throw IllegalStateException("No audio track found in file") - } + if (audioTrackIndex == -1 || format == null) { + throw IllegalStateException("No audio track found in file") + } - extractor.selectTrack(audioTrackIndex) - val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm" - val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) - val durationUs = format.getLong(MediaFormat.KEY_DURATION) - val duration = (durationUs / 1_000_000).toInt() + extractor.selectTrack(audioTrackIndex) + val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm" + val sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + val durationUs = format.getLong(MediaFormat.KEY_DURATION) + val duration = (durationUs / 1_000_000).toInt() - val decoder = MediaCodec.createDecoderByType(mime) - decoder.configure(format, null, null, 0) - decoder.start() + decoder = MediaCodec.createDecoderByType(mime) + decoder.configure(format, null, null, 0) + decoder.start() - val pcmSamples = mutableListOf() - val bufferInfo = MediaCodec.BufferInfo() - var inputDone = false - var outputDone = false + val pcmSamples = mutableListOf() + val bufferInfo = MediaCodec.BufferInfo() + var inputDone = false + var outputDone = false - while (!outputDone && currentCoroutineContext().isActive) { - if (!inputDone) { - val inputBufferIndex = decoder.dequeueInputBuffer(10000) - if (inputBufferIndex >= 0) { - val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!! - val sampleSize = extractor.readSampleData(inputBuffer, 0) - if (sampleSize < 0) { - decoder.queueInputBuffer( - inputBufferIndex, - 0, - 0, - 0, - MediaCodec.BUFFER_FLAG_END_OF_STREAM, - ) - inputDone = true - } else { - val presentationTimeUs = extractor.sampleTime - decoder.queueInputBuffer( - inputBufferIndex, - 0, - sampleSize, - presentationTimeUs, - 0, - ) - extractor.advance() - if (durationUs > 0) { - onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f)) + while (!outputDone && currentCoroutineContext().isActive) { + if (!inputDone) { + val inputBufferIndex = decoder.dequeueInputBuffer(10000) + if (inputBufferIndex >= 0) { + val inputBuffer = decoder.getInputBuffer(inputBufferIndex)!! + val sampleSize = extractor.readSampleData(inputBuffer, 0) + if (sampleSize < 0) { + decoder.queueInputBuffer( + inputBufferIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + val presentationTimeUs = extractor.sampleTime + decoder.queueInputBuffer( + inputBufferIndex, + 0, + sampleSize, + presentationTimeUs, + 0, + ) + extractor.advance() + if (durationUs > 0) { + onProgress((presentationTimeUs.toFloat() / durationUs).coerceIn(0f, 1f)) + } } } } + + val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000) + if (outputBufferIndex >= 0) { + val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!! + val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer() + while (shortBuffer.hasRemaining()) { + pcmSamples.add(shortBuffer.get() / 32768f) + } + decoder.releaseOutputBuffer(outputBufferIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + outputDone = true + } + } } - val outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000) - if (outputBufferIndex >= 0) { - val outputBuffer = decoder.getOutputBuffer(outputBufferIndex)!! - val shortBuffer = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer() - while (shortBuffer.hasRemaining()) { - pcmSamples.add(shortBuffer.get() / 32768f) - } - decoder.releaseOutputBuffer(outputBufferIndex, false) - if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { - outputDone = true - } - } + return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration) + } finally { + decoder?.stop() + decoder?.release() + extractor.release() } - - decoder.stop() - decoder.release() - extractor.release() - - return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration) } private fun processPcmWithTarsos( @@ -320,86 +323,90 @@ class VoiceAnonymizer { format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE) val encoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC) - encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) - encoder.start() - val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) - var audioTrackIndex = -1 var muxerStarted = false - val bufferInfo = MediaCodec.BufferInfo() - var inputOffset = 0 - var inputDone = false - var outputDone = false - val totalSamples = pcmData.size + try { + encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) + encoder.start() - while (!outputDone) { - if (!inputDone) { - val inputBufferIndex = encoder.dequeueInputBuffer(10000) - if (inputBufferIndex >= 0) { - val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!! - inputBuffer.clear() + var audioTrackIndex = -1 + val bufferInfo = MediaCodec.BufferInfo() + var inputOffset = 0 + var inputDone = false + var outputDone = false + val totalSamples = pcmData.size - val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset) - if (samplesToWrite <= 0) { - encoder.queueInputBuffer( - inputBufferIndex, - 0, - 0, - 0, - MediaCodec.BUFFER_FLAG_END_OF_STREAM, - ) - inputDone = true - } else { - for (i in 0 until samplesToWrite) { - val sample = - (pcmData[inputOffset + i] * 32767) - .toInt() - .coerceIn(-32768, 32767) - .toShort() - inputBuffer.putShort(sample) + while (!outputDone) { + if (!inputDone) { + val inputBufferIndex = encoder.dequeueInputBuffer(10000) + if (inputBufferIndex >= 0) { + val inputBuffer = encoder.getInputBuffer(inputBufferIndex)!! + inputBuffer.clear() + + val samplesToWrite = minOf((inputBuffer.capacity() / 2), pcmData.size - inputOffset) + if (samplesToWrite <= 0) { + encoder.queueInputBuffer( + inputBufferIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + for (i in 0 until samplesToWrite) { + val sample = + (pcmData[inputOffset + i] * 32767) + .toInt() + .coerceIn(-32768, 32767) + .toShort() + inputBuffer.putShort(sample) + } + val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate + encoder.queueInputBuffer( + inputBufferIndex, + 0, + inputBuffer.position(), + presentationTimeUs, + 0, + ) + inputOffset += samplesToWrite + onProgress(inputOffset.toFloat() / totalSamples) + } + } + } + + val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000) + when { + outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { + audioTrackIndex = muxer.addTrack(encoder.outputFormat) + muxer.start() + muxerStarted = true + } + + outputBufferIndex >= 0 -> { + val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!! + if (muxerStarted && bufferInfo.size > 0) { + outputBuffer.position(bufferInfo.offset) + outputBuffer.limit(bufferInfo.offset + bufferInfo.size) + muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo) + } + encoder.releaseOutputBuffer(outputBufferIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { + outputDone = true } - val presentationTimeUs = (inputOffset * 1_000_000L) / sampleRate - encoder.queueInputBuffer( - inputBufferIndex, - 0, - inputBuffer.position(), - presentationTimeUs, - 0, - ) - inputOffset += samplesToWrite - onProgress(inputOffset.toFloat() / totalSamples) } } } - - val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000) - when { - outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { - audioTrackIndex = muxer.addTrack(encoder.outputFormat) - muxer.start() - muxerStarted = true - } - - outputBufferIndex >= 0 -> { - val outputBuffer = encoder.getOutputBuffer(outputBufferIndex)!! - if (muxerStarted && bufferInfo.size > 0) { - outputBuffer.position(bufferInfo.offset) - outputBuffer.limit(bufferInfo.offset + bufferInfo.size) - muxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo) - } - encoder.releaseOutputBuffer(outputBufferIndex, false) - if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) { - outputDone = true - } - } + } finally { + encoder.stop() + encoder.release() + if (muxerStarted) { + muxer.stop() } + muxer.release() } - - encoder.stop() - encoder.release() - muxer.stop() - muxer.release() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index eea2a0af6..5d0099b89 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -74,10 +74,12 @@ class VoiceReplyViewModel : ViewModel() { scope = viewModelScope, logTag = "VoiceReplyViewModel", onError = { error -> - accountViewModel.toastManager.toast( - stringRes(Amethyst.instance.appContext, R.string.error), - error.message ?: "Voice anonymization failed", - ) + if (::accountViewModel.isInitialized) { + accountViewModel.toastManager.toast( + stringRes(Amethyst.instance.appContext, R.string.error), + error.message ?: "Voice anonymization failed", + ) + } }, ) From ec159ff85f8617429d30a27e7d1eb8e672717efa Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 12 Jan 2026 23:02:33 +0100 Subject: [PATCH 35/70] add translations --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 6 ++++++ amethyst/src/main/res/values-de-rDE/strings.xml | 6 ++++++ amethyst/src/main/res/values-pt-rBR/strings.xml | 6 ++++++ amethyst/src/main/res/values-sv-rSE/strings.xml | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 275889023..6c1aa14d8 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -1228,4 +1228,10 @@ Smazat seznam Smazat sadu doporučení Typy + Žádný + Hluboký + Vysoký + Neutrální + Anonymizovat + Upravuje výšku vašeho hlasu. Poznámka: základní změny výšky hlasu mohou být odhodlanými posluchači potenciálně zpětně rozpoznány. diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 7a521e907..586128b78 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -1233,4 +1233,10 @@ anz der Bedingungen ist erforderlich Liste löschen Empfehlungspaket löschen Typen + Keine + Tief + Hoch + Neutral + Anonymisieren + Verändert die Tonhöhe deiner Stimme. Hinweis: einfache Tonhöhenänderungen können von entschlossenen Zuhörern möglicherweise rückgängig gemacht werden. diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index a3f8b297c..97ff4065d 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -1228,4 +1228,10 @@ Excluir lista Excluir pacote de recomendações Tipos + Nenhum + Grave + Agudo + Neutro + Anonimizar + Altera o tom da sua voz. Nota: alterações básicas de tom podem potencialmente ser revertidas por ouvintes determinados. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 7a56f1c6e..de6dae781 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -1227,4 +1227,10 @@ Ta bort lista Ta bort rekommendationspaket Typer + Ingen + Djup + Hög + Neutral + Anonymisera + Ändrar tonhöjden på din röst. Obs: enkla förändringar av tonhöjd kan potentiellt återskapas av målmedvetna lyssnare. From 5241a6dfa79cedd1e0cdd54f3fb3e59b078f2d8d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 11:27:45 -0500 Subject: [PATCH 36/70] Fixes missing cache after AI PR for the desktop app Removes dependency on the Cache from User. Updates interface methods to match. --- .../amethyst/model/LocalCache.kt | 8 +++-- .../com/vitorpamplona/amethyst/model/User.kt | 3 -- .../reqCommand/user/UserObservers.kt | 5 +++- .../amethyst/commons/model/User.kt | 29 ++++--------------- .../commons/model/cache/ICacheProvider.kt | 3 +- 5 files changed, 18 insertions(+), 30 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 beac2ade5..34224a4b1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -324,7 +324,7 @@ object LocalCache : ILocalCache, ICacheProvider { return users.get(key) } - override fun countUsers(predicate: (String, Any) -> Boolean): Int { + override fun countUsers(predicate: (String, User) -> Boolean): Int { var count = 0 users.forEach { key, user -> if (predicate(key, user)) count++ @@ -531,9 +531,13 @@ object LocalCache : ILocalCache, ICacheProvider { // avoids processing empty contact lists. if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty() && (wasVerified || justVerify(event))) { - user.updateContactList(event) + val needsToUpdateFollowers = user.updateContactList(event) // Log.d("CL", "Consumed contact list ${user.toNostrUri()} ${event.relays()?.size}") + needsToUpdateFollowers.forEach { + getUserIfExists(it)?.flowSet?.followers?.invalidateData() + } + updateObservables(event) return true diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt index bfa68c725..860631c0c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -21,9 +21,6 @@ package com.vitorpamplona.amethyst.model // Re-export from commons for backwards compatibility -typealias UserDependencies = com.vitorpamplona.amethyst.commons.model.UserDependencies typealias User = com.vitorpamplona.amethyst.commons.model.User -typealias UserFlowSet = com.vitorpamplona.amethyst.commons.model.UserFlowSet typealias RelayInfo = com.vitorpamplona.amethyst.commons.model.RelayInfo -typealias UserBundledRefresherFlow = com.vitorpamplona.amethyst.commons.model.UserBundledRefresherFlow typealias UserState = com.vitorpamplona.amethyst.commons.model.UserState 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 d2d2b59da..7e0496b7c 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 @@ -36,6 +36,7 @@ import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl +import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent import com.vitorpamplona.quartz.nip51Lists.hashtagList.HashtagListEvent import kotlinx.collections.immutable.ImmutableList @@ -392,7 +393,9 @@ fun observeUserFollowerCount( .followers.stateFlow .sample(200) .mapLatest { userState -> - userState.user.transientFollowerCount() + LocalCache.countUsers { _, user -> + user.latestContactList?.isTaggedUser(user.pubkeyHex) ?: false + } }.distinctUntilChanged() .flowOn(Dispatchers.IO) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt index 287992db5..558cf168c 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/User.kt @@ -22,11 +22,11 @@ package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.nip56Reports.UserReportCache import com.vitorpamplona.amethyst.commons.model.trustedAssertions.UserCardsCache import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.lightning.Lud06 +import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata @@ -51,7 +51,6 @@ class User( val pubkeyHex: String, val nip65RelayListNote: Note, val dmRelayListNote: Note, - private val cacheProvider: ICacheProvider? = null, ) { private var reports: UserReportCache? = null private var cards: UserCardsCache? = null @@ -120,8 +119,8 @@ class User( fun profilePicture(): String? = info?.picture - fun updateContactList(event: ContactListEvent) { - if (event.id == latestContactList?.id) return + fun updateContactList(event: ContactListEvent): Set { + if (event.id == latestContactList?.id) return emptySet() val oldContactListEvent = latestContactList latestContactList = event @@ -129,20 +128,9 @@ class User( // Update following of the current user flowSet?.follows?.invalidateData() - // Update Followers of the past user list - // Update Followers of the new contact list - (oldContactListEvent)?.unverifiedFollowKeySet()?.forEach { - (cacheProvider?.getUserIfExists(it) as? User) - ?.flowSet - ?.followers - ?.invalidateData() - } - (latestContactList)?.unverifiedFollowKeySet()?.forEach { - (cacheProvider?.getUserIfExists(it) as? User) - ?.flowSet - ?.followers - ?.invalidateData() - } + val affectedUsers = event.verifiedFollowKeySet() + (oldContactListEvent?.verifiedFollowKeySet() ?: emptySet()) + + return affectedUsers } fun addZap( @@ -217,11 +205,6 @@ class User( fun transientFollowCount(): Int? = latestContactList?.unverifiedFollowKeySet()?.size - fun transientFollowerCount(): Int = - cacheProvider?.countUsers { _, it -> - (it as? User)?.latestContactList?.isTaggedUser(pubkeyHex) ?: false - } ?: 0 - fun reportsOrNull(): UserReportCache? = reports fun reports(): UserReportCache = reports ?: UserReportCache().also { reports = it } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt index 7d1f32f98..16d8fae4e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.model.cache +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey /** @@ -60,7 +61,7 @@ interface ICacheProvider { * @param predicate Filter function for counting users * @return Count of users matching the predicate */ - fun countUsers(predicate: (String, Any) -> Boolean): Int + fun countUsers(predicate: (String, User) -> Boolean): Int /** * Gets a Note if it exists in cache. From 6d15e4861c95f9dac8808a1ac75daa8bb73b0043 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 12:22:40 -0500 Subject: [PATCH 37/70] Moves channels to commons and removes the dependency in the cache from Note --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../amethyst/model/LocalCache.kt | 15 ++------- .../model/emphChat/EphemeralChatChannel.kt | 2 +- .../nip28PublicChats/PublicChatChannel.kt | 2 +- .../LiveActivitiesChannel.kt | 2 +- .../amethyst/model/privateChats/Chatroom.kt | 2 +- .../ChannelFinderFilterAssemblyGroup.kt | 2 +- ...ChannelFinderFilterAssemblySubscription.kt | 2 +- .../reqCommand/channel/ChannelObservers.kt | 4 +-- .../ChannelMetadataWatcherSubAssembler.kt | 2 +- .../LiveActivityWatcherSubAssembly.kt | 2 +- .../amethyst/ui/dal/ChangesFlowFilter.kt | 2 +- .../ui/feeds/ChannelFeedContentState.kt | 2 +- .../amethyst/ui/feeds/ChannelFeedState.kt | 2 +- .../privateDM/dal/ChatroomFeedViewModel.kt | 2 +- .../publicChannels/dal/ChannelFeedFilter.kt | 2 +- .../dal/ChannelFeedViewModel.kt | 2 +- .../datasource/ChannelFilterAssembler.kt | 2 +- .../ChannelFilterAssemblerSubscription.kt | 2 +- .../ChannelFromUserFilterSubAssembler.kt | 2 +- .../ChannelPublicFilterSubAssembler.kt | 2 +- .../send/ChannelNewMessageViewModel.kt | 2 +- .../loggedIn/home/dal/HomeLiveFilter.kt | 2 +- .../loggedIn/home/live/LiveStatusIndicator.kt | 2 +- .../amethyst/commons}/model/Channel.kt | 10 ++++-- .../amethyst/commons/model/IAccount.kt | 2 ++ .../amethyst/commons}/model/ListChange.kt | 2 +- .../amethyst/commons/model/Note.kt | 33 +++++++++++++------ .../commons/model/cache/ICacheProvider.kt | 17 ++-------- 29 files changed, 62 insertions(+), 65 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/Channel.kt (95%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/ListChange.kt (96%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index e2171228c..b0797e6c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1658,7 +1658,7 @@ class Account( fun isAllHidden(users: Set): Boolean = users.all { isHidden(it) } - fun isHidden(user: User) = isHidden(user.pubkeyHex) + override fun isHidden(user: User) = isHidden(user.pubkeyHex) fun isHidden(userHex: String): Boolean = hiddenUsers.flow.value.isUserHidden(userHex) 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 34224a4b1..f030b6898 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -23,8 +23,8 @@ package com.vitorpamplona.amethyst.model import android.util.LruCache import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider -import com.vitorpamplona.amethyst.commons.model.cache.IChannel import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel @@ -332,17 +332,6 @@ object LocalCache : ILocalCache, ICacheProvider { return count } - override fun getAnyChannel(note: Any?): IChannel? { - val channelNote = note as? Note ?: return null - val channel = getAnyChannel(channelNote) - // Wrap Channel to implement IChannel interface - return channel?.let { - object : IChannel { - override fun relays(): List? = it.relays().toList() - } - } - } - fun getAddressableNoteIfExists(key: String): AddressableNote? = Address.parse(key)?.let { addressables.get(it) } fun getAddressableNoteIfExists(address: Address): AddressableNote? = addressables.get(address) @@ -1409,7 +1398,7 @@ object LocalCache : ILocalCache, ICacheProvider { } } - fun getAnyChannel(note: Note): Channel? = note.event?.let { getAnyChannel(it) } + override fun getAnyChannel(note: Note): Channel? = note.event?.let { getAnyChannel(it) } fun getAnyChannel(noteEvent: Event): Channel? = when (noteEvent) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt index 12a28673d..99f001fca 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.emphChat import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt index 75fa41af7..2330dd92c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.nip28PublicChats import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.toShortDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt index d595164c2..df732bd66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.nip53LiveActivities import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.note.toShortDisplay diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt index c49b79eab..3850b3116 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.model.privateChats import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.ListChange +import com.vitorpamplona.amethyst.commons.model.ListChange import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.NotesGatherer import com.vitorpamplona.amethyst.model.User diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt index 5600979a5..3a46ae22e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblyGroup.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive.ChannelMetadataAndLiveActivityWatcherSubAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.ChannelLoaderSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt index eebcea166..7a5534b39 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelFinderFilterAssemblySubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 1add909d1..c251e230c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -24,8 +24,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.vitorpamplona.amethyst.model.Channel -import com.vitorpamplona.amethyst.model.ChannelState +import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.ChannelState import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt index 2932b0d86..f00993da3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index 0a3a9129d..c13bd4497 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt index dab6bd150..c9ca76ec6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChangesFlowFilter.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.dal -import com.vitorpamplona.amethyst.model.ListChange +import com.vitorpamplona.amethyst.commons.model.ListChange import kotlinx.coroutines.flow.MutableSharedFlow interface ChangesFlowFilter : IAdditiveFeedFilter { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt index cf62c2cf5..46b3c3fe0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedContentState.kt @@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.BundledUpdate diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt index 123a3e4d6..e6f003813 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/ChannelFeedState.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.feeds import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import kotlinx.coroutines.flow.MutableStateFlow @Stable diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt index 95173849d..a1f09c95a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/dal/ChatroomFeedViewModel.kt @@ -24,10 +24,10 @@ import androidx.compose.runtime.Stable import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.vitorpamplona.amethyst.commons.model.ListChange import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.ListChange import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt index db66af206..cff3e85d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChangesFlowFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt index 23c1c7fb7..94bf576dc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/dal/ChannelFeedViewModel.kt @@ -22,8 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.dal import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ListChangeFeedViewModel class ChannelFeedViewModel( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt index 8beefc621..91d249db6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.service.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelFromUserFilterSubAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies.ChannelPublicFilterSubAssembler diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt index 0dc9e50f8..e9ff59571 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/ChannelFilterAssemblerSubscription.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datas import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.service.relayClient.KeyDataSourceSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt index 700e04709..28bba13f2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt index f5730d428..2eb7bb5a7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 4d3a5b6b6..2bec459d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -33,9 +33,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 62f55d898..00f2a0e6e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index 8a9c2966f..cd7cb3030 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.produceState import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.Channel +import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.OnlineChecker diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt index 8ea68969b..5844cf49c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Channel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Channel.kt @@ -18,10 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model +package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.utils.cache.LargeCache @@ -32,6 +31,11 @@ import java.lang.ref.WeakReference @Stable abstract class Channel : NotesGatherer { + companion object { + val DefaultFeedOrder: Comparator = + compareByDescending { it.createdAt() }.thenBy { it.idHex } + } + val notes = LargeCache() var lastNote: Note? = null @@ -143,7 +147,7 @@ abstract class Channel : NotesGatherer { return toBeRemoved.toSet() } - fun pruneHiddenMessages(account: Account): Set { + fun pruneHiddenMessages(account: IAccount): Set { val hidden = notes .filter { key, it -> diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt index 7460b925a..ff2345b6d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/IAccount.kt @@ -84,4 +84,6 @@ interface IAccount { /** Set of followed user pubkeys (for feed ordering/highlighting) */ fun followingKeySet(): Set + + fun isHidden(user: User): Boolean } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ListChange.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ListChange.kt index 1c0530571..fe62b8659 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/ListChange.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/ListChange.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model +package com.vitorpamplona.amethyst.commons.model sealed class ListChange { data class Addition( diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt index 49af94c47..5ef037053 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/Note.kt @@ -22,13 +22,13 @@ package com.vitorpamplona.amethyst.commons.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable -import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.threading.checkNotInMainThread import com.vitorpamplona.amethyst.commons.util.firstFullCharOrEmoji import com.vitorpamplona.amethyst.commons.util.replace import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.experimental.bounties.addedRewardValue import com.vitorpamplona.quartz.experimental.bounties.hasAdditionalReward +import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.lightning.LnInvoiceUtil import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Event @@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent @@ -109,7 +110,6 @@ class AddressableNote( @Stable open class Note( val idHex: String, - private val cacheProvider: ICacheProvider? = null, ) : NotesGatherer { // These fields are only available after the Text Note event is received. // They are immutable after that. @@ -196,15 +196,28 @@ open class Note( } fun relayHintUrl(): NormalizedRelayUrl? { - val noteEvent = event - val communityPostRelays = - when (noteEvent) { - is CommunityDefinitionEvent -> noteEvent.relayUrls().ifEmpty { null }?.toSet() - is IsInPublicChatChannel -> cacheProvider?.getAnyChannel(this)?.relays() - else -> null + // checks Community Events first + when (val noteEvent = event) { + is CommunityDefinitionEvent -> noteEvent.relayUrls().firstOrNull()?.let { return it } + is IsInPublicChatChannel -> { + inGatherers?.forEach { + if (it is com.vitorpamplona.amethyst.commons.model.Channel) { + it.relays().firstOrNull()?.let { return it } + } + } } - - if (!communityPostRelays.isNullOrEmpty()) return (communityPostRelays as? Collection)?.firstOrNull() + is LiveActivitiesEvent -> { + noteEvent.relays().ifEmpty { null }?.toSet() + } + is LiveActivitiesChatMessageEvent -> { + inGatherers?.forEach { + if (it is com.vitorpamplona.amethyst.commons.model.Channel) { + it.relays().firstOrNull()?.let { return it } + } + } + } + is EphemeralChatEvent -> noteEvent.roomId()?.let { return it.relayUrl } + } val currentOutbox = author?.outboxRelays()?.toSet() diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt index 16d8fae4e..b62cae5c8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.commons.model.cache +import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey @@ -43,7 +45,7 @@ interface ICacheProvider { * @param note The note to look up channel for * @return The channel if found, null otherwise */ - fun getAnyChannel(note: Any?): IChannel? + fun getAnyChannel(note: Note): Channel? /** * Gets a User by public key hex. @@ -98,16 +100,3 @@ interface ICacheProvider { */ fun hasBeenDeleted(event: Any): Boolean } - -/** - * Minimal channel interface for relay resolution. - * Full channel implementations (PublicChatChannel, LiveActivitiesChannel) - * implement this interface. - */ -interface IChannel { - /** - * Gets the relay URLs for this channel. - * @return List of relay URLs or null if none configured - */ - fun relays(): List? -} From a8dd229fb23babc979ede8e3a2bef98b8b0eee45 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:30:51 -0500 Subject: [PATCH 38/70] Moves Channels (public chats, ephemeral channels and live streams) Account modules to commons --- .../vitorpamplona/amethyst/model/Account.kt | 12 ++++----- .../amethyst/model/AccountSettings.kt | 13 +++++++--- .../amethyst/model/LocalCache.kt | 10 ++++---- .../reqCommand/channel/ChannelObservers.kt | 4 +-- ...adataAndLiveActivityWatcherSubAssembler.kt | 4 +-- .../ChannelMetadataWatcherSubAssembler.kt | 2 +- .../FilterChannelMetadataCreationById.kt | 2 +- .../FilterChannelMetadataUpdatesById.kt | 2 +- .../FilterLiveStreamUpdatesByAddress.kt | 2 +- .../LiveActivityWatcherSubAssembly.kt | 2 +- .../reqCommand/user/UserObservers.kt | 4 +-- .../ui/navigation/routes/RouteMaker.kt | 6 ++--- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 6 ++--- .../amethyst/ui/note/NoteCompose.kt | 2 +- .../ui/screen/loggedIn/AccountViewModel.kt | 6 ++--- .../ChannelFromUserFilterSubAssembler.kt | 6 ++--- .../ChannelPublicFilterSubAssembler.kt | 6 ++--- .../FilterMessagesToEphemeralChat.kt | 2 +- .../FilterMessagesToLiveStream.kt | 2 +- .../FilterMessagesToPublicChat.kt | 2 +- .../FilterMyMessagesToEphemeralChat.kt | 2 +- .../FilterMyMessagesToLiveActivities.kt | 2 +- .../FilterMyMessagesToPublicChat.kt | 2 +- .../publicChannels/ephemChat/ChannelView.kt | 2 +- .../ephemChat/LoadEphemeralChatChannel.kt | 2 +- .../header/EphemeralChatChannelHeader.kt | 2 +- .../ephemChat/header/EphemeralChatTopBar.kt | 2 +- .../header/ShortEphemeralChatChannelHeader.kt | 2 +- .../header/actions/JoinChatButton.kt | 2 +- .../header/actions/LeaveChatButton.kt | 2 +- .../nip28PublicChat/ChannelView.kt | 2 +- .../header/LongPublicChatChannelHeader.kt | 2 +- .../header/PublicChatChannelHeader.kt | 2 +- .../header/PublicChatTopBar.kt | 2 +- .../header/ShortPublicChatChannelHeader.kt | 2 +- .../header/actions/EditChatButton.kt | 2 +- .../header/actions/JoinChatButton.kt | 2 +- .../header/actions/LeaveChatButton.kt | 2 +- .../header/actions/LinkChatButton.kt | 2 +- .../header/actions/OpenChatButton.kt | 2 +- .../header/actions/ShareChatButton.kt | 2 +- .../metadata/ChannelMetadataScreen.kt | 2 +- .../metadata/ChannelMetadataViewModel.kt | 2 +- .../nip53LiveActivities/ChannelView.kt | 2 +- .../nip53LiveActivities/ShowVideoStreaming.kt | 2 +- .../header/LiveActivitiesChannelHeader.kt | 2 +- .../header/LiveActivityTopBar.kt | 2 +- .../header/LongLiveActivityChannelHeader.kt | 2 +- .../header/ShortLiveActivityChannelHeader.kt | 2 +- .../send/ChannelFileUploadDialog.kt | 4 +-- .../send/ChannelNewMessageViewModel.kt | 6 ++--- .../chats/rooms/ChatroomHeaderCompose.kt | 4 +-- .../RenderPublicChatChannelThumb.kt | 2 +- .../ui/screen/loggedIn/home/HomeScreen.kt | 4 +-- .../loggedIn/home/dal/HomeLiveFilter.kt | 4 +-- .../loggedIn/home/live/LiveStatusIndicator.kt | 4 +-- .../home/live/RenderEphemeralBubble.kt | 2 +- .../home/live/RenderLiveActivityBubble.kt | 2 +- .../commons/model/cache/ICacheProvider.kt | 16 +++++++++++- .../model/emphChat/EphemeralChatChannel.kt | 2 +- .../EphemeralChatListDecryptionCache.kt | 2 +- .../model/emphChat/EphemeralChatListState.kt | 25 +++++++++++-------- .../nip28PublicChats/PublicChatChannel.kt | 8 +++--- .../PublicChatListDecryptionCache.kt | 2 +- .../nip28PublicChats/PublicChatListState.kt | 25 +++++++++++-------- .../nip38UserStatuses/UserStatusAction.kt | 2 +- .../LiveActivitiesChannel.kt | 8 +++--- 67 files changed, 153 insertions(+), 122 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/emphChat/EphemeralChatChannel.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/emphChat/EphemeralChatListDecryptionCache.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/emphChat/EphemeralChatListState.kt (88%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip28PublicChats/PublicChatChannel.kt (94%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip28PublicChats/PublicChatListDecryptionCache.kt (96%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip28PublicChats/PublicChatListState.kt (90%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip38UserStatuses/UserStatusAction.kt (97%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip53LiveActivities/LiveActivitiesChannel.kt (92%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b0797e6c7..fc51b78cc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -24,15 +24,18 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.commons.model.IAccount +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatListState import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListState -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListDecryptionCache -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatListState import com.vitorpamplona.amethyst.model.localRelays.LocalRelayListState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountHomeRelayState import com.vitorpamplona.amethyst.model.nip01UserMetadata.AccountOutboxRelayState @@ -46,9 +49,6 @@ import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListDecryptionCache -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index a654dc436..fb10ec4ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.amethyst.model import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatRepository +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListRepository import com.vitorpamplona.amethyst.ui.actions.mediaServers.DEFAULT_MEDIA_SERVERS import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerName import com.vitorpamplona.amethyst.ui.screen.FeedDefinition @@ -143,7 +145,8 @@ class AccountSettings( val lastReadPerRoute: MutableStateFlow>> = MutableStateFlow(mapOf()), var hasDonatedInVersion: MutableStateFlow> = MutableStateFlow(setOf()), val pendingAttestations: MutableStateFlow> = MutableStateFlow>(mapOf()), -) { +) : EphemeralChatRepository, + PublicChatListRepository { val saveable = MutableStateFlow(AccountSettingsUpdater(null)) val syncedSettings: AccountSyncedSettings = AccountSyncedSettings(AccountSyncedSettingsInternal()) @@ -389,7 +392,9 @@ class AccountSettings( } } - fun updateChannelListTo(newChannelList: ChannelListEvent?) { + override fun channelList() = backupChannelList + + override fun updateChannelListTo(newChannelList: ChannelListEvent?) { if (newChannelList == null || newChannelList.tags.isEmpty()) return // Events might be different objects, we have to compare their ids. @@ -429,7 +434,9 @@ class AccountSettings( } } - fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) { + override fun ephemeralChatList() = backupEphemeralChatList + + override fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) { if (newEphemeralChatList == null || newEphemeralChatList.tags.isEmpty()) return // Events might be different objects, we have to compare their ids. 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 f030b6898..fed602b28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -25,11 +25,11 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.isDebug -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag import com.vitorpamplona.amethyst.model.privateChats.ChatroomList @@ -459,7 +459,7 @@ object LocalCache : ILocalCache, ICacheProvider { fun getOrCreateAddressableNoteInternal(key: Address): AddressableNote = addressables.getOrCreate(key) { AddressableNote(key) } - fun getOrCreateAddressableNote(key: Address): AddressableNote { + override fun getOrCreateAddressableNote(key: Address): AddressableNote { val note = getOrCreateAddressableNoteInternal(key) // Loads the user outside a Syncronized block to avoid blocking if (note.author == null) { @@ -2717,7 +2717,7 @@ object LocalCache : ILocalCache, ICacheProvider { } } - fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true) + override fun justConsumeMyOwnEvent(event: Event) = justConsumeAndUpdateIndexes(event, null, true) fun justConsume( event: Event, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index c251e230c..f71d0b6f3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -26,9 +26,9 @@ import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.ChannelState +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt index 56781d1f7..07a032201 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/mixChatsLive/ChannelMetadataAndLiveActivityWatcherSubAssembler.kt @@ -20,8 +20,8 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.mixChatsLive -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.SingleSubEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats.filterChannelMetadataUpdatesById diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt index f00993da3..9c6094653 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/ChannelMetadataWatcherSubAssembler.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt index 884155614..a9c496d8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataCreationById.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt index 86c876a9e..1e4f9f3bb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip28PublicChats/FilterChannelMetadataUpdatesById.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip28PublicChats -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt index 07f98233b..e44356eef 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/FilterLiveStreamUpdatesByAddress.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel 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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt index c13bd4497..7de27e8b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/nip53LiveActivities/LiveActivityWatcherSubAssembly.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.nip53LiveActivities import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.ChannelFinderQueryState import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap 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 7e0496b7c..7ad81ba07 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 @@ -25,14 +25,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.NoteState import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt index 374c68d92..94deb91a8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/RouteMaker.kt @@ -20,13 +20,13 @@ */ package com.vitorpamplona.amethyst.ui.navigation.routes +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index ca5886d93..8e3dd0add 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -32,13 +32,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteOts import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.components.GenericLoadable 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 aa5928c7f..84cb7b979 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 @@ -53,9 +53,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelPicture import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeCommunityApprovalNeedStatus import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteEdits diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 8e1c4bcc4..fcfa5b4d6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -40,6 +40,9 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync import com.vitorpamplona.amethyst.commons.model.LiveHiddenUsers +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.Account @@ -50,9 +53,6 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.UiSettingsFlow import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.observables.CreatedAtComparator import com.vitorpamplona.amethyst.model.privacyOptions.EmptyRoleBasedHttpClientBuilder import com.vitorpamplona.amethyst.model.privacyOptions.IRoleBasedHttpClientBuilder diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt index 28bba13f2..98f043e84 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelFromUserFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt index 2eb7bb5a7..487f997df 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/ChannelPublicFilterSubAssembler.kt @@ -21,9 +21,9 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUniqueIdEoseManager import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelQueryState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt index f6cde11b5..e3d8e9f9a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToEphemeralChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt index 78aa37ce2..58d908227 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToLiveStream.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt index 256c3862a..c59ba6e59 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMessagesToPublicChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt index 84988cf43..dfed4a693 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToEphemeralChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.HexKey diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt index eef197437..165d00e26 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToLiveActivities.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt index 3fded09c0..3eaa0234a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/datasource/subassemblies/FilterMyMessagesToPublicChat.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.datasource.subassemblies -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt index 2a8adefca..de34a1df2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/ChannelView.kt @@ -30,8 +30,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt index b7c943c69..18c70810f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/LoadEphemeralChatChannel.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.note.produceStateIfNotNull import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt index 7a529fd86..8480f0073 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatChannelHeader.kt @@ -26,7 +26,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt index bcc891f4e..7ae747123 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/EphemeralChatTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt index 32fcefd1f..2bbeec845 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/ShortEphemeralChatChannelHeader.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt index 403881bd0..4b80c9cf2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/JoinChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt index ea4ff20c2..79a8133eb 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/ephemChat/header/actions/LeaveChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt index 89f03d6b4..e149d8380 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/ChannelView.kt @@ -30,8 +30,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadPublicChatChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt index e6946e763..5b0d32f43 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/LongPublicChatChannelHeader.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt index 3279af82d..de1904944 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatChannelHeader.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt index 2ba8c3ca0..ad7702f91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/PublicChatTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt index 22106578a..70d569541 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/ShortPublicChatChannelHeader.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel import com.vitorpamplona.amethyst.ui.components.LoadNote diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt index 37e17b278..78bbf142c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/EditChatButton.kt @@ -30,7 +30,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt index 230a8b4f5..d9964f55e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/JoinChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt index 10a623343..49a788879 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LeaveChatButton.kt @@ -24,7 +24,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt index abfd1b7ce..d57fb31ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/LinkChatButton.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt index 1a95f0364..6404c5074 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/OpenChatButton.kt @@ -33,7 +33,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.core.net.toUri import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt index 57013ff8a..f9e74b35a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/header/actions/ShareChatButton.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.njumpLink import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt index 8407ceb71..67cb04ab8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataScreen.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt index 7c9850706..e2260f14f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip28PublicChat/metadata/ChannelMetadataViewModel.kt @@ -31,9 +31,9 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt index 5c14da323..8176b91d1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ChannelView.kt @@ -29,8 +29,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LoadLiveActivityChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt index 98167d056..9a6106b87 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/ShowVideoStreaming.kt @@ -27,8 +27,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.layout.ContentScale +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.richtext.MediaUrlVideo -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelInfo import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.ZoomableContentView diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivitiesChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivitiesChannelHeader.kt index b9fe94e32..8bb6e64af 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivitiesChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivitiesChannelHeader.kt @@ -28,7 +28,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt index b2eeca605..056a59a8f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LiveActivityTopBar.kt @@ -21,7 +21,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header import androidx.compose.runtime.Composable -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt index 97fa81a62..9ad0b1e88 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/LongLiveActivityChannelHeader.kt @@ -40,9 +40,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt index b66031baa..4b6b8fb9f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/nip53LiveActivities/header/ShortLiveActivityChannelHeader.kt @@ -36,9 +36,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.note.LikeReaction diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt index 3e7b79bd3..ea40fa5fc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelFileUploadDialog.kt @@ -34,8 +34,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.navigation.navs.INav diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 2bec459d6..20c5b3f55 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -34,15 +34,15 @@ import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt index 86d975767..3bc08c640 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/rooms/ChatroomHeaderCompose.kt @@ -41,11 +41,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.model.nip11RelayInfo.loadRelayInfo -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteHasEvent import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserName diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt index 0d4c93a03..cb8deb029 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip28Chats/RenderPublicChatChannelThumb.kt @@ -41,10 +41,10 @@ import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import coil3.compose.AsyncImagePainter import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index e0bff36ad..7c55ca565 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -55,11 +55,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState import com.vitorpamplona.amethyst.model.AROUND_ME -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt index 00f2a0e6e..0e165c931 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeLiveFilter.kt @@ -21,11 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal import com.vitorpamplona.amethyst.commons.model.Channel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByOutboxTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsByProxyTopNavFilter import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsByOutboxTopNavFilter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt index cd7cb3030..b49297f35 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/LiveStatusIndicator.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.OnlineChecker import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import kotlinx.coroutines.Dispatchers diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt index 4a7886545..af28d1708 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderEphemeralBubble.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.emphChat.EphemeralChatChannel +import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelNoteAuthors import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt index 7e85e3504..556df134f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/live/RenderLiveActivityBubble.kt @@ -29,7 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observeChannelNoteAuthors import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt index b62cae5c8..721fdc6f8 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/cache/ICacheProvider.kt @@ -20,9 +20,12 @@ */ package com.vitorpamplona.amethyst.commons.model.cache +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.quartz.nip01Core.core.Address +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey /** @@ -81,7 +84,16 @@ interface ICacheProvider { * @param hexKey The note's ID in hex format * @return The Note (existing or newly created) */ - fun checkGetOrCreateNote(hexKey: HexKey): Any? + fun checkGetOrCreateNote(hexKey: HexKey): Note? + + /** + * Gets an existing AddressableNote or creates a new one if it doesn't exist. + * Used by ThreadAssembler for building thread structures. + * + * @param address The note's ID in address format + * @return The AddressableNote (existing or newly created) + */ + fun getOrCreateAddressableNote(key: Address): AddressableNote /** * Gets the event stream for cache updates. @@ -99,4 +111,6 @@ interface ICacheProvider { * @return true if the event has been deleted, false otherwise */ fun hasBeenDeleted(event: Any): Boolean + + fun justConsumeMyOwnEvent(event: Event): Boolean } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatChannel.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatChannel.kt index 99f001fca..ac8548a97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatChannel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.emphChat +package com.vitorpamplona.amethyst.commons.model.emphChat import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Channel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListDecryptionCache.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListDecryptionCache.kt index 60ff2f80f..c5f48b760 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListDecryptionCache.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.emphChat +package com.vitorpamplona.amethyst.commons.model.emphChat import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.experimental.ephemChat.list.roomSet diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt index 3899d0aef..cbdde212f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/emphChat/EphemeralChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/emphChat/EphemeralChatListState.kt @@ -18,12 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.emphChat +package com.vitorpamplona.amethyst.commons.model.emphChat -import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner @@ -41,12 +40,18 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch +interface EphemeralChatRepository { + fun ephemeralChatList(): EphemeralChatListEvent? + + fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?) +} + class EphemeralChatListState( val signer: NostrSigner, - val cache: LocalCache, + val cache: ICacheProvider, val decryptionCache: EphemeralChatListDecryptionCache, val scope: CoroutineScope, - val settings: AccountSettings, + val settings: EphemeralChatRepository, ) { // Creates a long-term reference for this note so that the GC doesn't collect the note it self val ephemeralChatListNote = cache.getOrCreateAddressableNote(getEphemeralChatListAddress()) @@ -58,7 +63,7 @@ class EphemeralChatListState( fun getEphemeralChatList(): EphemeralChatListEvent? = ephemeralChatListNote.event as? EphemeralChatListEvent suspend fun ephemeralChatListWithBackup(note: Note): Set { - val event = note.event as? EphemeralChatListEvent ?: settings.backupEphemeralChatList + val event = note.event as? EphemeralChatListEvent ?: settings.ephemeralChatList() return event?.let { decryptionCache.roomSet(it) } ?: emptySet() } @@ -109,11 +114,11 @@ class EphemeralChatListState( } init { - settings.backupEphemeralChatList?.let { event -> + settings.ephemeralChatList()?.let { event -> Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - LocalCache.justConsumeMyOwnEvent(event) + cache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt similarity index 94% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt index 2330dd92c..3e9b04b38 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatChannel.kt @@ -18,13 +18,13 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip28PublicChats +package com.vitorpamplona.amethyst.commons.model.nip28PublicChats import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.note.toShortDisplay +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt index a0a54ff93..4ba77febf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListDecryptionCache.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListDecryptionCache.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip28PublicChats +package com.vitorpamplona.amethyst.commons.model.nip28PublicChats import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt similarity index 90% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt index f52bd4687..4ac2acd91 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip28PublicChats/PublicChatListState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip28PublicChats/PublicChatListState.kt @@ -18,12 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip28PublicChats +package com.vitorpamplona.amethyst.commons.model.nip28PublicChats -import com.vitorpamplona.amethyst.model.AccountSettings -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent @@ -43,12 +42,18 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch +interface PublicChatListRepository { + fun channelList(): ChannelListEvent? + + fun updateChannelListTo(newChannelList: ChannelListEvent?) +} + class PublicChatListState( val signer: NostrSigner, - val cache: LocalCache, + val cache: ICacheProvider, val decryptionCache: PublicChatListDecryptionCache, val scope: CoroutineScope, - val settings: AccountSettings, + val settings: PublicChatListRepository, ) { // Creates a long-term reference for this note so that the GC doesn't collect the note it self val publicChatListNote = cache.getOrCreateAddressableNote(getChannelListAddress()) @@ -60,7 +65,7 @@ class PublicChatListState( fun getChannelList(): ChannelListEvent? = publicChatListNote.event as? ChannelListEvent suspend fun publicChatListWithBackup(note: Note): Set { - val event = note.event as? ChannelListEvent ?: settings.backupChannelList + val event = note.event as? ChannelListEvent ?: settings.channelList() return event?.let { decryptionCache.channelSet(it) } ?: emptySet() } @@ -124,11 +129,11 @@ class PublicChatListState( } init { - settings.backupChannelList?.let { event -> + settings.channelList()?.let { event -> Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}") @OptIn(DelicateCoroutinesApi::class) GlobalScope.launch(Dispatchers.IO) { - LocalCache.justConsumeMyOwnEvent(event) + cache.justConsumeMyOwnEvent(event) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt index 834e2437f..415e9f255 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip38UserStatuses/UserStatusAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip38UserStatuses +package com.vitorpamplona.amethyst.commons.model.nip38UserStatuses import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.quartz.nip01Core.core.Event diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt index df732bd66..a02287264 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip53LiveActivities/LiveActivitiesChannel.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip53LiveActivities/LiveActivitiesChannel.kt @@ -18,13 +18,13 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip53LiveActivities +package com.vitorpamplona.amethyst.commons.model.nip53LiveActivities import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.model.Channel -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.note.toShortDisplay +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User +import com.vitorpamplona.amethyst.commons.util.toShortDisplay import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress From afe66cd55fef527cce58d6caac9a4edfbda2cd12 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:31:04 -0500 Subject: [PATCH 39/70] Moves user actions to commons --- .../src/main/java/com/vitorpamplona/amethyst/model/Account.kt | 2 +- .../commons/model/nip38UserStatuses/UserStatusAction.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index fc51b78cc..16575da67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState +import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache @@ -50,7 +51,6 @@ import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState -import com.vitorpamplona.amethyst.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt index 415e9f255..725cb6c1a 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip38UserStatuses/UserStatusAction.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.commons.model.nip38UserStatuses -import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent From 9b7a83796b105c11dbbf4b2b704e229287c5b76c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:33:24 -0500 Subject: [PATCH 40/70] Moves private chatroom models to commons --- .../com/vitorpamplona/amethyst/model/LocalCache.kt | 2 +- .../ui/screen/loggedIn/DecryptAndIndexProcessor.kt | 2 +- .../commons}/model/privateChats/Chatroom.kt | 13 +++++++------ .../commons}/model/privateChats/ChatroomList.kt | 6 +++--- 4 files changed, 12 insertions(+), 11 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/privateChats/Chatroom.kt (92%) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/privateChats/ChatroomList.kt (95%) 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 fed602b28..a636b1068 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -28,11 +28,11 @@ import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel +import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.isDebug import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState import com.vitorpamplona.amethyst.model.observables.LatestByKindAndAuthor import com.vitorpamplona.amethyst.model.observables.LatestByKindWithETag -import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.service.BundledInsert import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.note.dateFormatter diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 0bf7a6cb9..2b0bb78fe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -20,10 +20,10 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn +import com.vitorpamplona.amethyst.commons.model.privateChats.ChatroomList import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.IEvent diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt similarity index 92% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt index 3850b3116..5485bf5bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/Chatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt @@ -18,14 +18,14 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.privateChats +package com.vitorpamplona.amethyst.commons.model.privateChats import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.Channel.Companion.DefaultFeedOrder import com.vitorpamplona.amethyst.commons.model.ListChange -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NotesGatherer -import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NotesGatherer +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent import com.vitorpamplona.quartz.nip14Subject.subject @@ -34,6 +34,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import java.lang.ref.WeakReference +import kotlin.collections.plus @Stable class Chatroom : NotesGatherer { @@ -66,7 +67,7 @@ class Chatroom : NotesGatherer { msg.author?.let { author -> if (author !in activeSenders) { - activeSenders += author + activeSenders + author } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatroomList.kt similarity index 95% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatroomList.kt index 6a81dcf37..40ba62fa1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/ChatroomList.kt @@ -18,10 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.privateChats +package com.vitorpamplona.amethyst.commons.model.privateChats -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable From 7c1053df2dd779180dddf87eea060d6f3a6f6f62 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:37:32 -0500 Subject: [PATCH 41/70] Moves reports to common Adds support for additional information --- .../java/com/vitorpamplona/amethyst/model/Account.kt | 5 +++-- .../amethyst/ui/screen/loggedIn/AccountViewModel.kt | 3 ++- .../commons}/model/nip56Reports/ReportAction.kt | 11 ++++++----- .../vitorpamplona/quartz/nip56Reports/ReportEvent.kt | 6 ++++-- 4 files changed, 15 insertions(+), 10 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip56Reports/ReportAction.kt (88%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 16575da67..aff2f7ea1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChann import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction +import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.logTime import com.vitorpamplona.amethyst.model.edits.PrivateStorageRelayListDecryptionCache @@ -77,7 +78,6 @@ import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListD import com.vitorpamplona.amethyst.model.nip51Lists.searchRelays.SearchRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.trustedRelays.TrustedRelayListState -import com.vitorpamplona.amethyst.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.model.nip65RelayList.Nip65RelayListState import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListDecryptionCache import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState @@ -571,7 +571,8 @@ class Account( suspend fun report( user: User, type: ReportType, - ) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, userProfile(), signer)) + content: String = "", + ) = sendMyPublicAndPrivateOutbox(ReportAction.report(user, type, content, userProfile(), signer)) suspend fun delete(note: Note) = delete(listOf(note)) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index fcfa5b4d6..fffbe41a2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -701,9 +701,10 @@ class AccountViewModel( fun report( user: User, type: ReportType, + content: String = "", ) { launchSigner { - account.report(user, type) + account.report(user, type, content) account.hideUser(user.pubkeyHex) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/ReportAction.kt similarity index 88% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/ReportAction.kt index 1dfc00f11..ffd1c21c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip56Reports/ReportAction.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip56Reports/ReportAction.kt @@ -18,10 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip56Reports +package com.vitorpamplona.amethyst.commons.model.nip56Reports -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip56Reports.ReportEvent import com.vitorpamplona.quartz.nip56Reports.ReportType @@ -31,6 +31,7 @@ class ReportAction { suspend fun report( user: User, type: ReportType, + content: String = "", by: User, signer: NostrSigner, ): ReportEvent? { @@ -39,7 +40,7 @@ class ReportAction { return null } - val template = ReportEvent.build(user.pubkeyHex, type) + val template = ReportEvent.build(user.pubkeyHex, type, content) return signer.sign(template) } @@ -57,7 +58,7 @@ class ReportAction { } return note.event?.let { - signer.sign(ReportEvent.build(it, type)) + signer.sign(ReportEvent.build(it, type, content)) } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt index f1f601c64..7a72281ff 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip56Reports/ReportEvent.kt @@ -77,8 +77,9 @@ class ReportEvent( fun build( reportedPost: Event, type: ReportType, + comment: String = "", createdAt: Long = TimeUtils.now(), - ) = eventTemplate(KIND, "", createdAt) { + ) = eventTemplate(KIND, comment, createdAt) { alt(ALT_PREFIX + type.code) event(reportedPost.id, type) user(reportedPost.pubKey, type) @@ -91,8 +92,9 @@ class ReportEvent( fun build( reportedUser: HexKey, type: ReportType, + comment: String = "", createdAt: Long = TimeUtils.now(), - ) = eventTemplate(KIND, "", createdAt) { + ) = eventTemplate(KIND, comment, createdAt) { alt(ALT_PREFIX + type.code) user(reportedUser, type) } From 42bdd1c831146ba34b54f06abd819be4bb97023e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 14:57:29 -0500 Subject: [PATCH 42/70] Emoji State to commons --- .../vitorpamplona/amethyst/model/Account.kt | 2 +- .../emojiSuggestions/EmojiSuggestionState.kt | 4 +- .../ShowEmojiSuggestionList.kt | 38 +++++++++---------- .../nip22Comments/CommentPostViewModel.kt | 10 ++--- .../nip22Comments/GenericCommentPostScreen.kt | 1 - .../privateDM/send/ChatNewMessageViewModel.kt | 10 ++--- .../chats/privateDM/send/NewGroupDMScreen.kt | 1 - .../send/PrivateMessageEditFieldRow.kt | 1 - .../send/ChannelNewMessageViewModel.kt | 10 ++--- .../chats/publicChannels/send/EditFieldRow.kt | 1 - .../nip99Classifieds/NewProductScreen.kt | 1 - .../nip99Classifieds/NewProductViewModel.kt | 10 ++--- .../loggedIn/home/ShortNotePostScreen.kt | 1 - .../loggedIn/home/ShortNotePostViewModel.kt | 10 ++--- .../publicMessages/NewPublicMessageScreen.kt | 1 - .../NewPublicMessageViewModel.kt | 10 ++--- .../model/nip30CustomEmojis/EmojiPackState.kt | 15 ++++---- 17 files changed, 59 insertions(+), 67 deletions(-) rename {amethyst/src/main/java/com/vitorpamplona/amethyst => commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons}/model/nip30CustomEmojis/EmojiPackState.kt (93%) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index aff2f7ea1..9b08ad91e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListDecryptionCache import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatListState +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip38UserStatuses.UserStatusAction import com.vitorpamplona.amethyst.commons.model.nip56Reports.ReportAction import com.vitorpamplona.amethyst.commons.richtext.RichTextParser @@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.model.nip02FollowLists.Kind3FollowListState import com.vitorpamplona.amethyst.model.nip03Timestamp.OtsState import com.vitorpamplona.amethyst.model.nip17Dms.DmInboxRelayState import com.vitorpamplona.amethyst.model.nip17Dms.DmRelayListState -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState import com.vitorpamplona.amethyst.model.nip51Lists.BookmarkListState import com.vitorpamplona.amethyst.model.nip51Lists.HiddenUsersState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/EmojiSuggestionState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/EmojiSuggestionState.kt index c3af93f09..f49a10eb3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/EmojiSuggestionState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/EmojiSuggestionState.kt @@ -20,14 +20,16 @@ */ package com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions +import androidx.compose.runtime.Stable +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn +@Stable class EmojiSuggestionState( val account: Account, ) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt index fd81456ac..a788a0488 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/creators/emojiSuggestions/ShowEmojiSuggestionList.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.spacedBy -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.heightIn @@ -43,10 +42,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState -import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.Size10dp @@ -57,7 +55,6 @@ fun ShowEmojiSuggestionList( emojiSuggestions: EmojiSuggestionState, onSelect: (EmojiPackState.EmojiMedia) -> Unit, onFullSize: (EmojiPackState.EmojiMedia) -> Unit, - accountViewModel: AccountViewModel, modifier: Modifier = Modifier.heightIn(0.dp, 200.dp), ) { val suggestions by emojiSuggestions.results.collectAsStateWithLifecycle(emptyList()) @@ -79,22 +76,23 @@ fun ShowEmojiSuggestionList( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = spacedBy(Size10dp), ) { - Box(Size40Modifier) { - UrlImageView(it.link, accountViewModel) - } + AsyncImage( + it.link, + contentDescription = it.code, + modifier = Size40Modifier, + ) Text(it.code, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) - Box(Size40Modifier, contentAlignment = Alignment.Center) { - IconButton( - onClick = { - onFullSize(it) - }, - ) { - Icon( - imageVector = Icons.Outlined.OpenInFull, - contentDescription = stringRes(R.string.use_direct_url), - modifier = Modifier.size(20.dp), - ) - } + IconButton( + modifier = Size40Modifier, + onClick = { + onFullSize(it) + }, + ) { + Icon( + imageVector = Icons.Outlined.OpenInFull, + contentDescription = stringRes(R.string.use_direct_url), + modifier = Modifier.size(20.dp), + ) } } HorizontalDivider( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt index 1a99000a7..0b634d26a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/CommentPostViewModel.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator @@ -435,7 +435,7 @@ open class CommentPostViewModel : myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag( it.code, - it.link.url, + it.link, ) } } @@ -634,11 +634,11 @@ open class CommentPostViewModel : } open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 6f5d1e758..74c923e82 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -357,7 +357,6 @@ private fun GenericCommentPostBody( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, modifier = Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index c2ce04d5e..eabc0d274 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -32,11 +32,11 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.ui.actions.NewMessageTagger import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -519,7 +519,7 @@ class ChatNewMessageViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -659,11 +659,11 @@ class ChatNewMessageViewModel : } fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt index ab72a1d18..ef9b3c7fa 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/NewGroupDMScreen.kt @@ -311,7 +311,6 @@ fun GroupDMScreenContent( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt index 34f637ef7..bf168bc2a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/PrivateMessageEditFieldRow.kt @@ -127,7 +127,6 @@ fun PrivateMessageEditFieldRow( it, channelScreenModel::autocompleteWithEmoji, channelScreenModel::autocompleteWithEmojiUrl, - accountViewModel, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt index 20c5b3f55..d0b2de1ab 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/ChannelNewMessageViewModel.kt @@ -36,13 +36,13 @@ import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord import com.vitorpamplona.amethyst.commons.model.Channel import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.UploadOrchestrator @@ -484,7 +484,7 @@ open class ChannelNewMessageViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -575,11 +575,11 @@ open class ChannelNewMessageViewModel : } open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt index 6cc1c9370..c03285025 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/publicChannels/send/EditFieldRow.kt @@ -104,7 +104,6 @@ fun EditFieldRow( it, channelScreenModel::autocompleteWithEmoji, channelScreenModel::autocompleteWithEmojiUrl, - accountViewModel, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt index 4128694c3..8a92c7f27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductScreen.kt @@ -327,7 +327,6 @@ private fun NewProductBody( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, modifier = Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index 90bf27713..af0b07c96 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator @@ -365,7 +365,7 @@ open class NewProductViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -534,11 +534,11 @@ open class NewProductViewModel : } open fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaDescription.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaDescription.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 65a2b051d..4318f13c8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -454,7 +454,6 @@ private fun NewPostScreenBody( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, modifier = Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index 538057970..90ee62b3a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -36,11 +36,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState.EmojiMedia import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.service.uploads.MediaCompressor @@ -719,7 +719,7 @@ open class ShortNotePostViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -929,11 +929,11 @@ open class ShortNotePostViewModel : } open fun autocompleteWithEmojiUrl(item: EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt index 1643a6798..629a97cba 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageScreen.kt @@ -284,7 +284,6 @@ fun PublicMessageScreenContent( it, postViewModel::autocompleteWithEmoji, postViewModel::autocompleteWithEmojiUrl, - accountViewModel, Modifier.heightIn(0.dp, 300.dp), ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index 19ba54057..b44ac347b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -33,11 +33,11 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.compose.currentWord import com.vitorpamplona.amethyst.commons.compose.insertUrlAtCursor import com.vitorpamplona.amethyst.commons.compose.replaceCurrentWord +import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.model.nip30CustomEmojis.EmojiPackState import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.uploads.MediaCompressor import com.vitorpamplona.amethyst.service.uploads.MultiOrchestrator @@ -385,7 +385,7 @@ class NewPublicMessageViewModel : ): List { if (myEmojiSet == null) return emptyList() return CustomEmoji.findAllEmojiCodes(message).mapNotNull { possibleEmoji -> - myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link.url) } + myEmojiSet.firstOrNull { it.code == possibleEmoji }?.let { EmojiUrlTag(it.code, it.link) } } } @@ -581,11 +581,11 @@ class NewPublicMessageViewModel : } fun autocompleteWithEmojiUrl(item: EmojiPackState.EmojiMedia) { - val wordToInsert = item.link.url + " " + val wordToInsert = item.link + " " viewModelScope.launch(Dispatchers.IO) { - iMetaAttachments.downloadAndPrepare(item.link.url) { - Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link.url) + iMetaAttachments.downloadAndPrepare(item.link) { + Amethyst.instance.roleBasedHttpClientBuilder.okHttpClientForImage(item.link) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt similarity index 93% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt index 52d0f1aee..d7d914594 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/nip30CustomEmojis/EmojiPackState.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/nip30CustomEmojis/EmojiPackState.kt @@ -18,12 +18,11 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.model.nip30CustomEmojis +package com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis -import com.vitorpamplona.amethyst.commons.richtext.MediaUrlImage -import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.model.NoteState +import com.vitorpamplona.amethyst.commons.model.Note +import com.vitorpamplona.amethyst.commons.model.NoteState +import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent @@ -43,12 +42,12 @@ import kotlinx.coroutines.flow.transformLatest class EmojiPackState( val signer: NostrSigner, - val cache: LocalCache, + val cache: ICacheProvider, val scope: CoroutineScope, ) { class EmojiMedia( val code: String, - val link: MediaUrlImage, + val link: String, ) // Creates a long-term reference for this note so that the GC doesn't collect the note it self @@ -84,7 +83,7 @@ class EmojiPackState( fun convertEmojiPack(pack: EmojiPackEvent): List = pack.taggedEmojis().map { - EmojiMedia(it.code, MediaUrlImage(it.url)) + EmojiMedia(it.code, it.url) } fun mergePack(list: Array): List = From 33c51b9a103e4edfd76b83ed641af8cd14e33c7a Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 13 Jan 2026 20:00:04 +0000 Subject: [PATCH 43/70] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-cs-rCZ/strings.xml | 12 ++++++------ amethyst/src/main/res/values-de-rDE/strings.xml | 11 +++++------ amethyst/src/main/res/values-pt-rBR/strings.xml | 12 ++++++------ amethyst/src/main/res/values-sv-rSE/strings.xml | 11 +++++------ 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/res/values-cs-rCZ/strings.xml b/amethyst/src/main/res/values-cs-rCZ/strings.xml index 6c1aa14d8..e97ff5381 100644 --- a/amethyst/src/main/res/values-cs-rCZ/strings.xml +++ b/amethyst/src/main/res/values-cs-rCZ/strings.xml @@ -172,6 +172,12 @@ Neočekávaný stav nahrávání NIP-95 zatím není pro hlasové zprávy podporován Nahrávání hlasu selhalo: %1$s + Žádný + Hluboký + Vysoký + Neutrální + Anonymizovat + Upravuje výšku vašeho hlasu. Poznámka: základní změny výšky hlasu mohou být odhodlanými posluchači potenciálně zpětně rozpoznány. Uživatel nemá nastavenou LN adresu pro přijímání sats "Odpověď zde…" Zkopíruje ID poznámky do schránky pro sdílení @@ -1228,10 +1234,4 @@ Smazat seznam Smazat sadu doporučení Typy - Žádný - Hluboký - Vysoký - Neutrální - Anonymizovat - Upravuje výšku vašeho hlasu. Poznámka: základní změny výšky hlasu mohou být odhodlanými posluchači potenciálně zpětně rozpoznány. diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml index 586128b78..e2cfb90cb 100644 --- a/amethyst/src/main/res/values-de-rDE/strings.xml +++ b/amethyst/src/main/res/values-de-rDE/strings.xml @@ -174,6 +174,11 @@ erie gespeichert Unerwarteter Upload-Status NIP-95 wird für Sprachnachrichten noch nicht unterstützt Sprach-Upload fehlgeschlagen: %1$s + Keine + Tief + Hoch + Anonymisieren + Verändert die Tonhöhe deiner Stimme. Hinweis: einfache Tonhöhenänderungen können von entschlossenen Zuhörern möglicherweise rückgängig gemacht werden. Der Benutzer hat keine Lightning-Adresse eingerichtet, um Sats zu empfangen "Hier antworten…" Kopiert die Notiz-ID zum Teilen in die Zwischenablage @@ -1233,10 +1238,4 @@ anz der Bedingungen ist erforderlich Liste löschen Empfehlungspaket löschen Typen - Keine - Tief - Hoch - Neutral - Anonymisieren - Verändert die Tonhöhe deiner Stimme. Hinweis: einfache Tonhöhenänderungen können von entschlossenen Zuhörern möglicherweise rückgängig gemacht werden. diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml index 97ff4065d..a90421341 100644 --- a/amethyst/src/main/res/values-pt-rBR/strings.xml +++ b/amethyst/src/main/res/values-pt-rBR/strings.xml @@ -172,6 +172,12 @@ Estado de upload inesperado O NIP-95 ainda não é suportado para mensagens de voz Falha no upload de voz: %1$s + Nenhum + Grave + Agudo + Neutro + Anonimizar + Altera o tom da sua voz. Nota: alterações básicas de tom podem potencialmente ser revertidas por ouvintes determinados. Usuário não tem um endereço lightning configurado para receber sats "responda aqui.. " Copia o ID do canal (note) para compartilhar @@ -1228,10 +1234,4 @@ Excluir lista Excluir pacote de recomendações Tipos - Nenhum - Grave - Agudo - Neutro - Anonimizar - Altera o tom da sua voz. Nota: alterações básicas de tom podem potencialmente ser revertidas por ouvintes determinados. diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index de6dae781..8a89c5993 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -172,6 +172,11 @@ Oväntat uppladdningstillstånd NIP-95 stöds ännu inte för röstmeddelanden Uppladdning av röst misslyckades: %1$s + Ingen + Djup + Hög + Anonymisera + Ändrar tonhöjden på din röst. Obs: enkla förändringar av tonhöjd kan potentiellt återskapas av målmedvetna lyssnare. Användaren har inte en Lightningadressinställning för att ta emot sats "svara här.. " Kopierar antecknings-ID till urklipp för delning @@ -1227,10 +1232,4 @@ Ta bort lista Ta bort rekommendationspaket Typer - Ingen - Djup - Hög - Neutral - Anonymisera - Ändrar tonhöjden på din röst. Obs: enkla förändringar av tonhöjd kan potentiellt återskapas av målmedvetna lyssnare. From ba5c86fbba549a1a55634a306f9cdac73669233b Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 15:31:26 -0500 Subject: [PATCH 44/70] Migrates rich text parser from jvm to commons --- .../amethyst/service/images/Base64Fetcher.kt | 2 +- .../commons/base64Image/Base64ImageBitmap.kt | 1 + .../ExpandableTextCutOffCalculator.kt | 0 .../commons/richtext/GalleryParser.kt | 0 .../commons/richtext/MediaContentModels.kt | 0 .../amethyst/commons/richtext/Patterns.kt | 15 ++-- .../commons/richtext/RichTextParser.kt | 70 +++++++++---------- .../richtext/RichTextParserSegments.kt | 0 .../commons/base64Image/Base64Image.kt | 36 ---------- .../base64Image/Base64ImagePlatform.jvm.kt | 1 + 10 files changed, 47 insertions(+), 78 deletions(-) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt (100%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt (86%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt (88%) rename commons/src/{jvmAndroid => commonMain}/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt (100%) delete mode 100644 commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt index bbab146da..41a89a151 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt @@ -30,8 +30,8 @@ import coil3.fetch.Fetcher import coil3.fetch.ImageFetchResult import coil3.key.Keyer import coil3.request.Options -import com.vitorpamplona.amethyst.commons.base64Image.Base64Image import com.vitorpamplona.amethyst.commons.base64Image.toBitmap +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.sha256.sha256 diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt index 9340c5d08..c5b8f2ca3 100644 --- a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt @@ -24,6 +24,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import java.util.Base64 fun Base64Image.toBitmap(content: String): Bitmap { diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt similarity index 86% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt index 5e9ddefbb..b54a9c760 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.richtext -import java.util.regex.Pattern - /** * Pattern constants for email and phone validation. * These replace android.util.Patterns for KMP compatibility. @@ -30,16 +28,21 @@ object Patterns { /** * Email address pattern from RFC 5322... From android.util.Patterns. */ - val EMAIL_ADDRESS: Pattern = - Pattern.compile( + val EMAIL_ADDRESS: Regex = + Regex( "[a-zA-Z0-9+._%-]{1,256}@[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}(\\.[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25})+", ) /** * Phone number pattern - matches common phone formats. */ - val PHONE: Pattern = - Pattern.compile( + val PHONE: Regex = + Regex( "^[+]?[(]?[0-9]{1,4}[)]?[-\\s./0-9]*\$", ) + + val BASE64_IMAGE: Regex = + Regex( + "data:image/(${RichTextParser.imageExtensions.joinToString(separator = "|")});base64,([a-zA-Z0-9+/]+={0,2})", + ) } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt similarity index 88% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 243a07957..f51662962 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.commons.richtext import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetectorOptions -import com.vitorpamplona.amethyst.commons.base64Image.Base64Image import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists @@ -45,8 +44,8 @@ import kotlinx.collections.immutable.toPersistentList import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL -import java.util.regex.Pattern import kotlin.coroutines.cancellation.CancellationException +import kotlin.text.iterator class RichTextParser { fun createMediaContent( @@ -108,7 +107,7 @@ class RichTextParser { return urls.mapNotNullTo(LinkedHashSet(urls.size)) { if (it.originalUrl.contains("@")) { - if (Patterns.EMAIL_ADDRESS.matcher(it.originalUrl).matches()) { + if (Patterns.EMAIL_ADDRESS.matches(it.originalUrl)) { null } else { it.originalUrl @@ -201,7 +200,7 @@ class RichTextParser { }.toImmutableList() } - private fun isNumber(word: String) = numberPattern.matcher(word).matches() + private fun isNumber(word: String) = numberPattern.matches(word) private fun isPhoneNumberChar(c: Char): Boolean = when (c) { @@ -225,7 +224,7 @@ class RichTextParser { return isPotentialNumber } - fun isDate(word: String): Boolean = shortDatePattern.matcher(word).matches() || longDatePattern.matcher(word).matches() + fun isDate(word: String): Boolean = shortDatePattern.matches(word) || longDatePattern.matches(word) private fun isArabic(text: String): Boolean = text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } @@ -239,7 +238,9 @@ class RichTextParser { ): Segment { if (word.isEmpty()) return RegularTextSegment(word) - if (word.startsWith("data:image/") && Base64Image.isBase64(word)) return Base64Segment(word) + if (word.startsWith("data:image/")) { + if (Patterns.BASE64_IMAGE.matches(word)) return Base64Segment(word) + } if (images.contains(word)) return ImageSegment(word) @@ -260,25 +261,22 @@ class RichTextParser { if (EmojiCoder.isCoded(word)) return SecretEmoji(word) if (word.contains("@")) { - if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) return EmailSegment(word) + if (Patterns.EMAIL_ADDRESS.matches(word)) return EmailSegment(word) } if (startsWithNIP19Scheme(word)) return BechSegment(word) if (isPotentialPhoneNumber(word) && !isDate(word)) { - if (Patterns.PHONE.matcher(word).matches()) return PhoneSegment(word) + if (Patterns.PHONE.matches(word)) return PhoneSegment(word) } val indexOfPeriod = word.indexOf(".") if (indexOfPeriod > 0 && indexOfPeriod < word.length - 1) { // periods cannot be the last one - val schemelessMatcher = noProtocolUrlValidator.matcher(word) - if (schemelessMatcher.find()) { - val url = schemelessMatcher.group(1) // url - val additionalChars = schemelessMatcher.group(4).ifEmpty { null } // additional chars - val pattern = - """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""" - .toRegex(RegexOption.IGNORE_CASE) - if (pattern.find(word) != null && url != null) { + val schemelessMatcher = noProtocolUrlValidator.find(word) + if (schemelessMatcher != null) { + val url = schemelessMatcher.groups[1]?.value // url + val additionalChars = schemelessMatcher.groups[4]?.value?.ifEmpty { null } // additional chars + if (additionalUrlSchema.find(word) != null && url != null) { return SchemelessUrlSegment(word, url, additionalChars) } } @@ -292,12 +290,11 @@ class RichTextParser { tags: ImmutableListOfLists, ): Segment { // First #[n] - - val matcher = tagIndex.matcher(word) try { - if (matcher.find()) { - val index = matcher.group(1)?.toInt() - val suffix = matcher.group(2) + val matcher = tagIndex.find(word) + if (matcher != null) { + val index = matcher.groups[1]?.value?.toInt() + val suffix = matcher.groups[2]?.value if (index != null && index >= 0 && index < tags.lists.size) { val tag = tags.lists[index] @@ -317,13 +314,12 @@ class RichTextParser { } // Second #Amethyst - val hashtagMatcher = hashTagsPattern.matcher(word) - try { - if (hashtagMatcher.find()) { - val hashtag = hashtagMatcher.group(1) + val hashtagMatcher = hashTagsPattern.find(word) + if (hashtagMatcher != null) { + val hashtag = hashtagMatcher.groups[1]?.value if (hashtag != null) { - return HashTagSegment(word, hashtag, hashtagMatcher.group(2).ifEmpty { null }) + return HashTagSegment(word, hashtag, hashtagMatcher.groups[2]?.value?.ifEmpty { null }) } } } catch (e: Exception) { @@ -335,22 +331,26 @@ class RichTextParser { } companion object { - val longDatePattern: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$") - val shortDatePattern: Pattern = Pattern.compile("^\\d{2}-\\d{2}-\\d{2}$") - val numberPattern: Pattern = Pattern.compile("^(-?[\\d.]+)([a-zA-Z%]*)$") + val longDatePattern: Regex = Regex("^\\d{4}-\\d{2}-\\d{2}$") + val shortDatePattern: Regex = Regex("^\\d{2}-\\d{2}-\\d{2}$") + val numberPattern: Regex = Regex("^(-?[\\d.]+)([a-zA-Z%]*)$") // Android9 seems to have an issue starting this regex. val noProtocolUrlValidator = try { - Pattern.compile( + Regex( "(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+[^\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}])*\\/?)(.*)", ) } catch (e: Exception) { - Pattern.compile( + Regex( "(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)", ) } + val additionalUrlSchema = + """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""" + .toRegex(RegexOption.IGNORE_CASE) + val HTTPRegex = "^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?" .toRegex(RegexOption.IGNORE_CASE) @@ -361,9 +361,9 @@ class RichTextParser { val imageExtensions = imageExt + imageExt.map { it.uppercase() } val videoExtensions = videoExt + videoExt.map { it.uppercase() } - val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)") - val hashTagsPattern: Pattern = - Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE) + val tagIndex = Regex("\\#\\[([0-9]+)\\](.*)") + val hashTagsPattern: Regex = + Regex("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", RegexOption.IGNORE_CASE) val acceptedNIP19schemes = listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1", "nembed") + @@ -436,6 +436,6 @@ class RichTextParser { } } - fun isUrlWithoutScheme(url: String) = noProtocolUrlValidator.matcher(url).matches() + fun isUrlWithoutScheme(url: String) = noProtocolUrlValidator.matches(url) } } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt deleted file mode 100644 index ac7bef89c..000000000 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.commons.base64Image - -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import java.util.regex.Pattern - -object Base64Image { - val pattern: Pattern = - Pattern.compile( - "data:image/(${RichTextParser.imageExtensions.joinToString(separator = "|")});base64,([a-zA-Z0-9+/]+={0,2})", - ) - - fun isBase64(content: String): Boolean { - val matcher = pattern.matcher(content) - return matcher.find() - } -} diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt index 6c523a33e..50fc059a2 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.base64Image import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import java.io.ByteArrayInputStream import java.util.Base64 import javax.imageio.ImageIO From 73fe985a65e38dc1da71b0699267d3a597c0f148 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 15:49:08 -0500 Subject: [PATCH 45/70] Fixes tests --- .../commons/base64Image/Base64ImageBitmap.kt | 12 +----- .../amethyst/commons/richtext/Base64Image.kt | 41 +++++++++++++++++++ .../base64Image/Base64ImagePlatform.jvm.kt | 17 +++----- 3 files changed, 48 insertions(+), 22 deletions(-) create mode 100644 commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt index c5b8f2ca3..fc7a2c232 100644 --- a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt @@ -25,18 +25,10 @@ import android.graphics.BitmapFactory import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage import com.vitorpamplona.amethyst.commons.richtext.Base64Image -import java.util.Base64 fun Base64Image.toBitmap(content: String): Bitmap { - val matcher = pattern.matcher(content) - - if (matcher.find()) { - val base64String = matcher.group(2) - val byteArray = Base64.getDecoder().decode(base64String) - return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) - } - - throw Exception("Unable to convert base64 to image $content") + val byteArray = parse(content) + return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size) } /** diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt new file mode 100644 index 000000000..b588ea572 --- /dev/null +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.richtext + +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import java.util.Base64 + +object Base64Image { + val pattern = Patterns.BASE64_IMAGE + + fun isBase64(content: String): Boolean = Patterns.BASE64_IMAGE.matches(content) + + fun parse(content: String): ByteArray { + val matcher = pattern.find(content) + if (matcher != null) { + val base64String = matcher.groups[2]?.value + val byteArray = Base64.getDecoder().decode(base64String) + return byteArray + } + + throw Exception("Unable to convert base64 to image $content") + } +} diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt index 50fc059a2..a5d23e969 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt @@ -24,23 +24,16 @@ import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage import com.vitorpamplona.amethyst.commons.richtext.Base64Image import java.io.ByteArrayInputStream -import java.util.Base64 import javax.imageio.ImageIO /** * Converts a base64 image data URI to a PlatformImage (BufferedImage wrapper). */ fun Base64Image.toPlatformImage(content: String): PlatformImage { - val matcher = pattern.matcher(content) + val byteArray = parse(content) - if (matcher.find()) { - val base64String = matcher.group(2) - val byteArray = Base64.getDecoder().decode(base64String) - val bufferedImage = - ImageIO.read(ByteArrayInputStream(byteArray)) - ?: throw Exception("Unable to decode base64 image: $content") - return bufferedImage.toPlatformImage() - } - - throw Exception("Unable to convert base64 to image $content") + val bufferedImage = + ImageIO.read(ByteArrayInputStream(byteArray)) + ?: throw Exception("Unable to decode base64 image: $content") + return bufferedImage.toPlatformImage() } From 09d0f4425cecb43fe43320d9fb9c9c5debb23355 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 16:04:02 -0500 Subject: [PATCH 46/70] Moves some of the stuff that was saved in Commons back to the desktop app because it is only used there --- .../com/vitorpamplona/amethyst/desktop/Main.kt | 8 ++++---- .../amethyst/desktop}/account/AccountManager.kt | 7 ++++--- .../network/DesktopRelayConnectionManager.kt | 2 +- .../desktop}/network/RelayConnectionManager.kt | 4 ++-- .../amethyst/desktop}/network/RelayStatus.kt | 2 +- .../desktop}/subscriptions/FeedSubscription.kt | 2 +- .../desktop}/subscriptions/FilterBuilders.kt | 2 +- .../desktop}/subscriptions/ProfileSubscription.kt | 2 +- .../desktop}/subscriptions/SubscriptionUtils.kt | 4 ++-- .../amethyst/desktop/ui/ComposeNoteDialog.kt | 2 +- .../amethyst/desktop/ui/DevSettingsSection.kt | 2 +- .../amethyst/desktop/ui}/EventExtensions.kt | 4 ++-- .../amethyst/desktop/ui/FeedScreen.kt | 15 +++++++-------- .../amethyst/desktop/ui/LoginScreen.kt | 8 ++++---- .../amethyst/desktop/ui/NoteActions.kt | 2 +- .../amethyst/desktop/ui/NotificationsScreen.kt | 6 +++--- .../amethyst/desktop/ui/ThreadScreen.kt | 11 +++++------ .../amethyst/desktop/ui/UserProfileScreen.kt | 10 +++++----- .../amethyst/desktop}/ui/auth/KeyInputField.kt | 4 ++-- .../amethyst/desktop}/ui/auth/LoginCard.kt | 4 ++-- .../desktop}/ui/auth/NewKeyWarningCard.kt | 4 ++-- .../amethyst/desktop}/ui/note/NoteCard.kt | 2 +- .../desktop}/ui/profile/ProfileInfoCard.kt | 2 +- .../amethyst/desktop}/ui/relay/RelayStatusCard.kt | 4 ++-- .../network/DesktopRelayConnectionManagerTest.kt | 2 +- 25 files changed, 57 insertions(+), 58 deletions(-) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/account/AccountManager.kt (98%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/network/RelayConnectionManager.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/network/RelayStatus.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/subscriptions/FeedSubscription.kt (98%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/subscriptions/FilterBuilders.kt (99%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/subscriptions/ProfileSubscription.kt (98%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/subscriptions/SubscriptionUtils.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui}/EventExtensions.kt (94%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/auth/KeyInputField.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/auth/LoginCard.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/auth/NewKeyWarningCard.kt (97%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/note/NoteCard.kt (99%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/profile/ProfileInfoCard.kt (98%) rename {commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop}/ui/relay/RelayStatusCard.kt (97%) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index 86b01d92d..7493784ee 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -73,12 +73,10 @@ import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState -import com.vitorpamplona.amethyst.commons.account.AccountManager -import com.vitorpamplona.amethyst.commons.account.AccountState -import com.vitorpamplona.amethyst.commons.ui.profile.ProfileInfoCard -import com.vitorpamplona.amethyst.commons.ui.relay.RelayStatusCard import com.vitorpamplona.amethyst.commons.ui.screens.MessagesPlaceholder import com.vitorpamplona.amethyst.commons.ui.screens.SearchPlaceholder +import com.vitorpamplona.amethyst.desktop.account.AccountManager +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.amethyst.desktop.ui.ComposeNoteDialog import com.vitorpamplona.amethyst.desktop.ui.FeedScreen @@ -86,6 +84,8 @@ import com.vitorpamplona.amethyst.desktop.ui.LoginScreen import com.vitorpamplona.amethyst.desktop.ui.NotificationsScreen import com.vitorpamplona.amethyst.desktop.ui.ThreadScreen import com.vitorpamplona.amethyst.desktop.ui.UserProfileScreen +import com.vitorpamplona.amethyst.desktop.ui.profile.ProfileInfoCard +import com.vitorpamplona.amethyst.desktop.ui.relay.RelayStatusCard import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt index a2cffbd11..60c902d78 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/account/AccountManager.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.account +package com.vitorpamplona.amethyst.desktop.account import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.commons.keystorage.SecureKeyStorage @@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip19Bech32.toNsec import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import java.io.File sealed class AccountState { data object LoggedOut : AccountState() @@ -221,8 +222,8 @@ class AccountManager private constructor( getPrefsFile().delete() } - private fun getPrefsFile(): java.io.File { + private fun getPrefsFile(): File { val homeDir = System.getProperty("user.home") - return java.io.File(homeDir, ".amethyst/last_account.txt") + return File(homeDir, ".amethyst/last_account.txt") } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt index 34dd8db54..18e7a9873 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt @@ -20,7 +20,7 @@ */ package com.vitorpamplona.amethyst.desktop.network -import com.vitorpamplona.amethyst.commons.network.RelayConnectionManager +import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket /** diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt index 3e0f7159d..8ef7207a2 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayConnectionManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayConnectionManager.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.network +package com.vitorpamplona.amethyst.desktop.network import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient @@ -65,7 +65,7 @@ open class RelayConnectionManager( } fun addRelay(url: String): NormalizedRelayUrl? { - val normalized = RelayUrlNormalizer.normalizeOrNull(url) ?: return null + val normalized = RelayUrlNormalizer.Companion.normalizeOrNull(url) ?: return null updateRelayStatus(normalized) { it.copy(connected = false, error = null) } return normalized } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayStatus.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayStatus.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt index f446c7eba..7b485a1ad 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/network/RelayStatus.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/RelayStatus.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.network +package com.vitorpamplona.amethyst.desktop.network import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt index 60852ec49..016d2eff4 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FeedSubscription.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FeedSubscription.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.subscriptions +package com.vitorpamplona.amethyst.desktop.subscriptions import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt similarity index 99% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt index 011d910fa..cd0766e10 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/FilterBuilders.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/FilterBuilders.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.subscriptions +package com.vitorpamplona.amethyst.desktop.subscriptions import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ProfileSubscription.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ProfileSubscription.kt index 2355c0fac..be45a7911 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/ProfileSubscription.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/ProfileSubscription.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.subscriptions +package com.vitorpamplona.amethyst.desktop.subscriptions import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt index 269901810..c0f592610 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/subscriptions/SubscriptionUtils.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/subscriptions/SubscriptionUtils.kt @@ -18,12 +18,12 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.subscriptions +package com.vitorpamplona.amethyst.desktop.subscriptions import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember -import com.vitorpamplona.amethyst.commons.network.RelayConnectionManager +import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.IRequestListener import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt index aca7e6664..80690495c 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ComposeNoteDialog.kt @@ -43,8 +43,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.model.nip10TextNotes.PublishAction +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt index 6c25bd22d..16813a366 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DevSettingsSection.kt @@ -51,7 +51,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState +import com.vitorpamplona.amethyst.desktop.account.AccountState import java.awt.Toolkit import java.awt.datatransfer.StringSelection diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt similarity index 94% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt index 0c392b8f8..954864647 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/util/EventExtensions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/EventExtensions.kt @@ -18,9 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.util +package com.vitorpamplona.amethyst.desktop.ui -import com.vitorpamplona.amethyst.commons.ui.note.NoteDisplayData +import com.vitorpamplona.amethyst.desktop.ui.note.NoteDisplayData import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip19Bech32.toNpub diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index 00cef6714..79a92ddbe 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -52,17 +52,16 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.state.EventCollectionState -import com.vitorpamplona.amethyst.commons.subscriptions.FeedMode -import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createFollowingFeedSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createGlobalFeedSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState -import com.vitorpamplona.amethyst.commons.ui.note.NoteCard -import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.FeedMode +import com.vitorpamplona.amethyst.desktop.subscriptions.createContactListSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createFollowingFeedSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createGlobalFeedSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription +import com.vitorpamplona.amethyst.desktop.ui.note.NoteCard import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt index be84b6375..bf32e84b5 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/LoginScreen.kt @@ -38,10 +38,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.SharedRes -import com.vitorpamplona.amethyst.commons.account.AccountManager -import com.vitorpamplona.amethyst.commons.account.AccountState -import com.vitorpamplona.amethyst.commons.ui.auth.LoginCard -import com.vitorpamplona.amethyst.commons.ui.auth.NewKeyWarningCard +import com.vitorpamplona.amethyst.desktop.account.AccountManager +import com.vitorpamplona.amethyst.desktop.account.AccountState +import com.vitorpamplona.amethyst.desktop.ui.auth.LoginCard +import com.vitorpamplona.amethyst.desktop.ui.auth.NewKeyWarningCard import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt index fcc12c918..2a96f8399 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NoteActions.kt @@ -39,11 +39,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.icons.Reply import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.model.nip18Reposts.RepostAction import com.vitorpamplona.amethyst.commons.model.nip25Reactions.ReactionAction +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager import com.vitorpamplona.quartz.nip01Core.core.Event import kotlinx.coroutines.Dispatchers diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt index 772f05219..82b75329a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -46,17 +46,17 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.icons.Reply import com.vitorpamplona.amethyst.commons.icons.Repost import com.vitorpamplona.amethyst.commons.icons.Zap import com.vitorpamplona.amethyst.commons.state.EventCollectionState -import com.vitorpamplona.amethyst.commons.subscriptions.createNotificationsSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.feed.FeedHeader import com.vitorpamplona.amethyst.commons.util.toTimeAgo +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.createNotificationsSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt index 0c469f085..4b6390f42 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -50,16 +50,15 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.state.EventCollectionState -import com.vitorpamplona.amethyst.commons.subscriptions.createNoteSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createThreadRepliesSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState -import com.vitorpamplona.amethyst.commons.ui.note.NoteCard import com.vitorpamplona.amethyst.commons.ui.thread.drawReplyLevel -import com.vitorpamplona.amethyst.commons.util.toNoteDisplayData +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.createNoteSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createThreadRepliesSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription +import com.vitorpamplona.amethyst.desktop.ui.note.NoteCard import com.vitorpamplona.quartz.nip01Core.core.Event /** diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 475fe55b1..d08286dee 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -56,17 +56,17 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.account.AccountState import com.vitorpamplona.amethyst.commons.model.nip02FollowList.FollowAction import com.vitorpamplona.amethyst.commons.state.EventCollectionState import com.vitorpamplona.amethyst.commons.state.FollowState -import com.vitorpamplona.amethyst.commons.subscriptions.createContactListSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createMetadataSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.createUserPostsSubscription -import com.vitorpamplona.amethyst.commons.subscriptions.rememberSubscription import com.vitorpamplona.amethyst.commons.ui.components.LoadingState import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar +import com.vitorpamplona.amethyst.desktop.account.AccountState import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager +import com.vitorpamplona.amethyst.desktop.subscriptions.createContactListSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createMetadataSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.createUserPostsSubscription +import com.vitorpamplona.amethyst.desktop.subscriptions.rememberSubscription import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt index 21e0c4b94..b71a3077d 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/KeyInputField.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/KeyInputField.kt @@ -18,8 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.ui.auth +package com.vitorpamplona.amethyst.desktop.ui.auth +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -43,7 +44,6 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.SharedRes import dev.icerock.moko.resources.compose.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview /** * Text field for entering Nostr keys (nsec or npub) with visibility toggle. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt index f97e9179b..4271baaf7 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/LoginCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/LoginCard.kt @@ -18,8 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.ui.auth +package com.vitorpamplona.amethyst.desktop.ui.auth +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -45,7 +46,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.SharedRes import dev.icerock.moko.resources.compose.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview /** * Login card with Nostr key input field and action buttons. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt index 9952ee4fb..b473580a4 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/auth/NewKeyWarningCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/auth/NewKeyWarningCard.kt @@ -18,8 +18,9 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.ui.auth +package com.vitorpamplona.amethyst.desktop.ui.auth +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -38,7 +39,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.commons.SharedRes import dev.icerock.moko.resources.compose.stringResource -import org.jetbrains.compose.ui.tooling.preview.Preview /** * Warning card displayed after generating a new Nostr key pair. diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt similarity index 99% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt index 67a70a500..e40940c9a 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/note/NoteCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.ui.note +package com.vitorpamplona.amethyst.desktop.ui.note import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/profile/ProfileInfoCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/ProfileInfoCard.kt similarity index 98% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/profile/ProfileInfoCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/ProfileInfoCard.kt index 53bf73bbc..d6023e4eb 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/profile/ProfileInfoCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/profile/ProfileInfoCard.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.ui.profile +package com.vitorpamplona.amethyst.desktop.ui.profile import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/relay/RelayStatusCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/RelayStatusCard.kt similarity index 97% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/relay/RelayStatusCard.kt rename to desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/RelayStatusCard.kt index c82c5102b..59dafe047 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/ui/relay/RelayStatusCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/relay/RelayStatusCard.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.ui.relay +package com.vitorpamplona.amethyst.desktop.ui.relay import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -41,7 +41,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.vitorpamplona.amethyst.commons.network.RelayStatus +import com.vitorpamplona.amethyst.desktop.network.RelayStatus /** * Card displaying the status of a Nostr relay connection. diff --git a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt index 3075e610a..d77211c0b 100644 --- a/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManagerTest.kt @@ -44,7 +44,7 @@ class DesktopRelayConnectionManagerTest { fun testRelayConnectionManagerInheritsFromBaseClass() { val manager = DesktopRelayConnectionManager() assertTrue( - manager is com.vitorpamplona.amethyst.commons.network.RelayConnectionManager, + manager is RelayConnectionManager, "DesktopRelayConnectionManager should extend RelayConnectionManager", ) } From 04ac40fa1ae0dfc9bb2323b0c8d85be8085efca8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 16:07:03 -0500 Subject: [PATCH 47/70] moves test as well --- .../amethyst/desktop}/filters/FilterBuildersTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename {commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons => desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop}/filters/FilterBuildersTest.kt (98%) diff --git a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/filters/FilterBuildersTest.kt similarity index 98% rename from commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt rename to desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/filters/FilterBuildersTest.kt index 4afb6e0ea..8392599e2 100644 --- a/commons/src/commonTest/kotlin/com/vitorpamplona/amethyst/commons/filters/FilterBuildersTest.kt +++ b/desktopApp/src/jvmTest/kotlin/com/vitorpamplona/amethyst/desktop/filters/FilterBuildersTest.kt @@ -18,10 +18,10 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.commons.filters +package com.vitorpamplona.amethyst.desktop.filters -import com.vitorpamplona.amethyst.commons.subscriptions.FilterBuilders -import com.vitorpamplona.amethyst.commons.subscriptions.buildFilter +import com.vitorpamplona.amethyst.desktop.subscriptions.FilterBuilders +import com.vitorpamplona.amethyst.desktop.subscriptions.buildFilter import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull From b0210e98f1ed6687ad7395628aec6646695ef23a Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 13 Jan 2026 21:08:51 +0000 Subject: [PATCH 48/70] New Crowdin translations by GitHub Action --- amethyst/src/main/res/values-sv-rSE/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml index 8a89c5993..44571cb2a 100644 --- a/amethyst/src/main/res/values-sv-rSE/strings.xml +++ b/amethyst/src/main/res/values-sv-rSE/strings.xml @@ -175,6 +175,7 @@ Ingen Djup Hög + Neutral Anonymisera Ändrar tonhöjden på din röst. Obs: enkla förändringar av tonhöjd kan potentiellt återskapas av målmedvetna lyssnare. Användaren har inte en Lightningadressinställning för att ta emot sats From be7865d458409541fc010f34cc70e13a46812792 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 17:13:03 -0500 Subject: [PATCH 49/70] updates keyring and vico --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f26e8b51..38e7607b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,7 +23,7 @@ firebaseBom = "34.7.0" fragmentKtx = "1.8.9" gms = "4.4.4" jacksonModuleKotlin = "2.20.1" -javaKeyring = "1.0.1" +javaKeyring = "1.0.4" jna = "5.18.1" jtorctl = "0.4.5.7" junit = "4.13.2" @@ -54,7 +54,7 @@ torAndroid = "0.4.8.21.1" translate = "17.0.3" unifiedpush = "3.0.10" urlDetector = "0.1.23" -vico-charts = "2.4.0" +vico-charts = "2.4.1" zelory = "3.0.1" zoomable = "2.9.0" zxing = "3.5.4" From 6097a0b5b7c2f5c8028d21ad6dace9aab51c16ab Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 13 Jan 2026 17:32:38 -0500 Subject: [PATCH 50/70] Removes the need for an extra object for the tag array in the custom emoji nip --- .../amethyst/ui/components/ClickableRoute.kt | 4 ++-- .../amethyst/commons/richtext/RichTextParser.kt | 2 +- .../quartz/nip30CustomEmoji/CustomEmoji.kt | 13 ++++++------- .../quartz/nip30CustomEmoji/Nip30Test.kt | 3 +-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt index f0c758bf3..1d4264585 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/components/ClickableRoute.kt @@ -375,7 +375,7 @@ fun CustomEmojiChecker( onEmojiText: @Composable (ImmutableList) -> Unit, ) { val mayContainEmoji by remember(text, tags) { - mutableStateOf(CustomEmoji.fastMightContainEmoji(text, tags)) + mutableStateOf(CustomEmoji.fastMightContainEmoji(text, tags?.lists)) } if (mayContainEmoji) { @@ -385,7 +385,7 @@ fun CustomEmojiChecker( } LaunchedEffect(text, tags) { - val newEmojiList = CustomEmoji.assembleAnnotatedList(text, tags) + val newEmojiList = CustomEmoji.assembleAnnotatedList(text, tags?.lists) if (newEmojiList != null) { emojiList = newEmojiList } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index f51662962..70d90061d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -140,7 +140,7 @@ class RichTextParser { val imageUrls = imagesForPager.filterValues { it is MediaUrlImage }.keys val videoUrls = imagesForPager.filterValues { it is MediaUrlVideo }.keys - val emojiMap = CustomEmoji.createEmojiMap(tags) + val emojiMap = CustomEmoji.createEmojiMap(tags.lists) val segments = findTextSegments(content, imageUrls, videoUrls, urlSet, emojiMap, tags) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt index b7d6a1b14..648e23519 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/CustomEmoji.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.quartz.nip30CustomEmoji import androidx.compose.runtime.Immutable -import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -32,10 +31,10 @@ class CustomEmoji { fun fastMightContainEmoji( input: String, - allTags: ImmutableListOfLists?, + allTags: Array>?, ): Boolean { if (allTags == null) return false - if (allTags.lists.any { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }) return true + if (allTags.any { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }) return true return input.contains(":") } @@ -47,7 +46,7 @@ class CustomEmoji { return input.contains(":") } - fun createEmojiMap(tags: ImmutableListOfLists): Map = tags.lists.filter { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }.associate { ":${it[1]}:" to it[2] } + fun createEmojiMap(tags: Array>): Map = tags.filter { it.size > 2 && it[0] == EmojiUrlTag.TAG_NAME }.associate { ":${it[1]}:" to it[2] } fun findAllEmojis(input: String): List { val matcher = customEmojiPattern.findAll(input) @@ -67,11 +66,11 @@ class CustomEmoji { fun assembleAnnotatedList( input: String, - allTags: ImmutableListOfLists?, + tags: Array>?, ): ImmutableList? { - if (allTags == null || allTags.lists.isEmpty()) return null + if (tags == null || tags.isEmpty()) return null - val emojiPairs = createEmojiMap(allTags) + val emojiPairs = createEmojiMap(tags) return assembleAnnotatedList(input, emojiPairs) } diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/Nip30Test.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/Nip30Test.kt index 6d443a2ea..41a8e2b07 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/Nip30Test.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip30CustomEmoji/Nip30Test.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.quartz.nip30CustomEmoji -import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -150,7 +149,7 @@ class Nip30Test { "#ioメシヨソイゲーム\n" + "https://misskey.io/play/9g3qza4jow" - val result = CustomEmoji.assembleAnnotatedList(input, ImmutableListOfLists(tags)) + val result = CustomEmoji.assembleAnnotatedList(input, tags) assertEquals(9, result!!.size) From c41040848ca577308d48576c55328afc9bb4d820 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 09:30:53 -0500 Subject: [PATCH 51/70] Fixes NPE on null existing event. --- .../java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 67b8afb51..0af8d19bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -106,12 +106,11 @@ class AntiSpamFilter { recentAddressables.put(hash, address) } else { // normal event + val existingEvent = recentEventIds[hash] if ( - (recentEventIds[hash] != null && recentEventIds[hash] != event.id) || + (existingEvent != null && existingEvent != event.id) || (spamMessages[hash] != null && !spamMessages[hash].duplicatedEventIds.contains(event.id)) ) { - val existingEvent = recentEventIds[hash] - val link1 = njumpLink(NEvent.create(existingEvent, null, null, relay)) val link2 = njumpLink(NEvent.create(event.id, null, null, relay)) From 54ab70beb77c9498975c61b783e89e79d2ba9d66 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 14:24:57 -0500 Subject: [PATCH 52/70] Fixes https://github.com/vitorpamplona/amethyst/issues/1670 --- fastlane/metadata/android/en-US/full_description.txt | 8 +++++++- fastlane/metadata/android/fr-FR/full_description.txt | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index ea06490eb..011c0e9fd 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1 +1,7 @@ -

Amethyst brings the best social network to your Android phone. Just insert your Nostr private key and start posting.

Nostr is an open protocol that is able to create a censorship-resistant global "social" network, transferring notes and other stuff using relays. It doesn't rely on any trusted central server, hence it is resilient; it is based on cryptographic keys and signatures, so it is tamperproof; it does not rely on P2P techniques, therefore it works. \ No newline at end of file +

Amethyst brings the best social network to your Android phone. Just insert your Nostr +private key and start posting.

+

Nostr +is an open protocol that is able to create a censorship-resistant global "social" network, transferring +notes and other stuff using relays. It doesn't rely on any trusted central server, hence it is resilient; +it is based on cryptographic keys and signatures, so it is tamperproof; it does not rely on P2P techniques, +therefore it works.

diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt index 1db1b2cf4..eafa7aa1d 100644 --- a/fastlane/metadata/android/fr-FR/full_description.txt +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -1 +1,8 @@ -

Amethyst apporte le meilleur réseau social sur votre téléphone Android. Insérez simplement votre clé privée Nostr et commencez à poster.

Nostr est un protocole ouvert qui permet de créer un réseau "social" mondial résistant à la censure, en transférant des notes et d'autres éléments à l'aide de relais. Il ne repose pas sur un serveur central de confiance, ce qui le rend résistant; il est basé sur des clés et des signatures cryptographiques, ce qui le rend inviolable; il ne repose pas sur des techniques P2P, ce qui le rend efficace. \ No newline at end of file +

Amethyst apporte le meilleur réseau social sur votre téléphone Android. +Insérez simplement votre clé privée Nostr et commencez à poster.

+ +

Nostr est un protocole +ouvert qui permet de créer un réseau "social" mondial résistant à la censure, en transférant des +notes et d'autres éléments à l'aide de relais. Il ne repose pas sur un serveur central de confiance, +ce qui le rend résistant; il est basé sur des clés et des signatures cryptographiques, ce qui le rend +inviolable; il ne repose pas sur des techniques P2P, ce qui le rend efficace.

\ No newline at end of file From adb2f48fb54005d679eda96088cefdb20d195dfc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 15:29:13 -0500 Subject: [PATCH 53/70] Updates event store readme --- .../quartz/nip01Core/store/README.md | 62 +++++-------------- 1 file changed, 16 insertions(+), 46 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md index c8084f0a2..b5c097136 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md @@ -5,10 +5,10 @@ This module implements an **Event Store** with nostr-native queries. The goal was not to make the fastest database, since there could be multiple optimizations made if consistency can be sacrificed, but a database that will never crash and never go corrupt. -## Responsibilities +## Features - **Storage & Retrieval**: - Stores Nostr events and enables retrieval using Nostr filters + Stores Nostr events and retrieves using Nostr filters - **Replaceable Events**: - Old versions are removed when newer versions arrive. @@ -18,35 +18,42 @@ consistency can be sacrificed, but a database that will never crash and never go - Ephemeral events never stored. - **NIP-40 Expirations** - - Manages expiration timestamps and prunes expired events. + - Prunes expired events. - Blocks expired events from being reinserted - **NIP-09 Deletion Events** - Deletes by event id - Deletes by address until the `created_at` - Blocks deleted events from being re-inserted. + - GiftWraps are deleted by p-tag - **NIP-62 Right to Vanish** - Supports deleting an entire user until the `created_at` for enhanced privacy + - GiftWraps are deleted by p-tag + +- **NIP-45 Counts**: + - Counts records matching Nostr filters - **NIP-50 Full Text Search**: - - Implements content indexing and full text search supporting rich queries over event content. + - Custom content/tag indexing + - Rich queries over event content and tags + - Indexes updated on replaceables, deletions, vanish and expirations. + +- **NIP-91: AND operator for tags**: + - Allows queries matching two or more tags at the same time - **Immutable Tables** - Triggers ensure event immutability. + - Tables cannot be updated, only inserted and deleted. ## Indexing Strategy The store indexes events using five dedicated tables: - `event_headers`: stores the canonical event fields. -- `event_tags`: indexes tag values for fast filtering on tag-based queries. +- `event_tags`: indexes tag values as a hash for fast filtering on tag-based queries. - `event_fts`: for the content of full text search - `event_expirations`: to control when expired events must be deleted. - `event_vanish`: to control up to when vanished accounts must be blocked. -SQL triggers ensure the **immutability of stored events**, preventing accidental or intentional -modifications. - ## Querying This module supports optimized query planning, producing efficient SQL for multi-filter evaluation @@ -63,43 +70,6 @@ store.query( ) ``` -Becomes - -```sql -SELECT id, pubkey, created_at, kind, tags, content, sig FROM event_headers -INNER JOIN ( - SELECT event_headers.row_id AS row_id - FROM event_headers - ORDER BY created_at DESC,id ASC - LIMIT 10 - - UNION - - SELECT event_headers.row_id AS row_id - FROM event_headers - INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id - WHERE event_headers.kind IN (1, 1111) - AND event_headers.pubkey = hexkey - AND event_fts MATCH "keywords" - ORDER BY created_at DESC, id ASC - LIMIT 100 - - UNION - - SELECT event_headers.row_id AS row_id - FROM event_headers - INNER JOIN event_fts ON event_fts.event_header_row_id = event_headers.row_id - WHERE event_headers.kind = 20 - AND event_fts MATCH "cats" - ORDER BY created_at DESC,id ASC - LIMIT 30 -) AS filtered ON event_headers.row_id = filtered.row_id -ORDER BY created_at DESC,id -``` - -The union operations support complex filter lists while avoiding redundant data fetching and -duplicated outstreams. - ## How to Use The `EventStore` class provides a high-level interface for interacting with the event database. From 3d0b0c01b9be2ab5081a13ecea42319f815918ae Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 15:36:32 -0500 Subject: [PATCH 54/70] Finishes editing quartz event store readme --- .../vitorpamplona/quartz/nip01Core/store/README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md index b5c097136..b82888b11 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/store/README.md @@ -11,14 +11,20 @@ consistency can be sacrificed, but a database that will never crash and never go Stores Nostr events and retrieves using Nostr filters - **Replaceable Events**: + - Forces unique constraint by kind, pubkey - Old versions are removed when newer versions arrive. - Old versions are blocked if newer versions exist. +- **Addressable Events**: + - Forces unique constraint by kind, pubkey, d-tag + - Old versions are removed when newer versions arrive. + - Old versions are blocked if newer versions exist. + - **Ephemeral Events** - Ephemeral events never stored. - **NIP-40 Expirations** - - Prunes expired events. + - Deletes expired events. - Blocks expired events from being reinserted - **NIP-09 Deletion Events** @@ -28,7 +34,8 @@ consistency can be sacrificed, but a database that will never crash and never go - GiftWraps are deleted by p-tag - **NIP-62 Right to Vanish** - - Supports deleting an entire user until the `created_at` for enhanced privacy + - Deletes all user events until the `created_at` + - Blocks vanished events from being re-inserted. - GiftWraps are deleted by p-tag - **NIP-45 Counts**: From 92d4654b209874869e939b87e1521f0bbee2d905 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 14 Jan 2026 16:49:11 -0500 Subject: [PATCH 55/70] Removes the memory counter methods because they are not the best way to measure the size in memory of these objects, since we intern strings all the time. --- .../com/vitorpamplona/amethyst/DebugUtils.kt | 12 ++++++++++ .../speedLogger/RelaySpeedLogger.kt | 3 ++- .../quartz/nip01Core/core/Address.kt | 8 ------- .../interactiveStories/tags/RootSceneTag.kt | 9 -------- .../quartz/nip01Core/core/Address.kt | 2 -- .../quartz/nip01Core/core/Event.kt | 11 ---------- .../quartz/nip01Core/hints/EventHintBundle.kt | 7 ------ .../quartz/nip01Core/tags/aTag/ATag.kt | 9 -------- .../quartz/nip01Core/tags/dTag/DTag.kt | 5 ----- .../quartz/nip01Core/tags/events/ETag.kt | 8 ------- .../quartz/nip01Core/tags/people/PTag.kt | 7 ------ .../quartz/nip02FollowList/tags/ContactTag.kt | 8 ------- .../quartz/nip13Pow/tags/PoWTag.kt | 4 ---- .../nip18Reposts/quotes/QAddressableTag.kt | 7 ------ .../quartz/nip18Reposts/quotes/QEventTag.kt | 8 ------- .../nip28PublicChat/list/tags/ChannelTag.kt | 8 ------- .../quartz/nip46RemoteSigner/BunkerMessage.kt | 4 +--- .../quartz/nip46RemoteSigner/BunkerRequest.kt | 11 +--------- .../nip46RemoteSigner/BunkerResponse.kt | 11 +--------- .../quartz/nip47WalletConnect/Response.kt | 18 +++------------ .../quartz/nip48ProxyTags/ProxyTag.kt | 4 ---- .../bookmarkList/tags/AddressBookmark.kt | 4 ---- .../bookmarkList/tags/EventBookmark.kt | 8 ------- .../nip51Lists/muteList/tags/UserTag.kt | 7 ------ .../nip51Lists/muteList/tags/WordTag.kt | 6 ----- .../meetingSpaces/tags/MeetingSpaceTag.kt | 4 ---- .../presence/tags/MeetingRoomTag.kt | 4 ---- .../quartz/nip57Zaps/LnZapEvent.kt | 6 ----- .../nip57Zaps/zapraiser/ZapRaiserTag.kt | 5 ----- .../nip59Giftwrap/seals/SealedRumorEvent.kt | 6 ----- .../nip59Giftwrap/wraps/GiftWrapEvent.kt | 6 ----- .../quartz/nip71Video/tags/TextTrackTag.kt | 7 ------ .../approval/tags/ApprovedAddressTag.kt | 4 ---- .../follow/tags/CommunityTag.kt | 4 ---- .../definition/AppDefinitionEvent.kt | 2 -- .../definition/AppMetadata.kt | 22 ------------------- .../NIP90ContentDiscoveryResponseEvent.kt | 6 ----- .../quartz/nip01Core/core/Address.ios.kt | 9 -------- .../quartz/nip01Core/core/Address.jvm.kt | 9 -------- 39 files changed, 20 insertions(+), 263 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt index 178950005..84277731f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/DebugUtils.kt @@ -26,8 +26,11 @@ import android.content.pm.ApplicationInfo import android.os.Debug import androidx.core.content.getSystemService import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes import kotlin.time.DurationUnit import kotlin.time.measureTimedValue @@ -228,3 +231,12 @@ inline fun debug( Log.d(tag, debugMessage()) } } + +fun Event.countMemory(): Int = + 7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) + 12 + // createdAt + kind + id.bytesUsedInMemory() + + pubKey.bytesUsedInMemory() + + tags.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } + + content.bytesUsedInMemory() + + sig.bytesUsedInMemory() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt index 8883fe5d8..94a2c05bd 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/speedLogger/RelaySpeedLogger.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.EventMessage import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.bytesUsedInMemory /** * Listens to NostrClient's onNotify messages from the relay @@ -47,7 +48,7 @@ class RelaySpeedLogger( msg: Message, ) { if (msg is EventMessage) { - current.increment(msg.event.kind, msg.subId, relay.url, msg.event.countMemory()) + current.increment(msg.event.kind, msg.subId, relay.url, msgStr.bytesUsedInMemory()) } } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt index 0152e00e6..1df2a6dbf 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt @@ -23,8 +23,6 @@ package com.vitorpamplona.quartz.nip01Core.core import android.os.Parcel import android.os.Parcelable import androidx.compose.runtime.Stable -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Stable actual data class Address actual constructor( @@ -35,12 +33,6 @@ actual data class Address actual constructor( Parcelable { actual fun toValue() = assemble(kind, pubKeyHex, dTag) - actual fun countMemory(): Int = - 3 * pointerSizeInBytes + - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() - actual override fun compareTo(other: Address): Int { val kindComparison = kind.compareTo(other.kind) return if (kindComparison == 0) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt index 3e55232b9..42d8c35ef 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/interactiveStories/tags/RootSceneTag.kt @@ -27,9 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class RootSceneTag( @@ -48,13 +46,6 @@ data class RootSceneTag( this.relay = relayHint } - fun countMemory(): Int = - 5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = assembleATagId(kind, pubKeyHex, dTag) fun toTagArray() = assemble(kind, pubKeyHex, dTag, relay) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt index dca489a28..555d4ce8a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.kt @@ -33,8 +33,6 @@ expect class Address( fun toValue(): String - fun countMemory(): Int - companion object { fun assemble( kind: Int, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt index e7a455689..4e1c373ea 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable open class Event( @@ -44,15 +42,6 @@ open class Event( */ open fun isContentEncoded() = false - open fun countMemory(): Int = - 7 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) - 12 + // createdAt + kind - id.bytesUsedInMemory() + - pubKey.bytesUsedInMemory() + - tags.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } + - content.bytesUsedInMemory() + - sig.bytesUsedInMemory() - fun toJson(): String = OptimizedJsonMapper.toJson(this) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt index 415d295b2..875a1395a 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/hints/EventHintBundle.kt @@ -24,8 +24,6 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class EventHintBundle( @@ -39,10 +37,5 @@ data class EventHintBundle( this.authorHomeRelay = authorHomeRelay } - fun countMemory(): Int = - 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) - event.countMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(event.id, event.pubKey, event.kind, relay) } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/ATag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/ATag.kt index 54925b539..809fc8698 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/ATag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/aTag/ATag.kt @@ -32,9 +32,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ATag( @@ -45,13 +43,6 @@ data class ATag( ) { constructor(address: Address, relayHint: NormalizedRelayUrl? = null) : this(address.kind, address.pubKeyHex, address.dTag, relayHint) - fun countMemory(): Int = - 5 * pointerSizeInBytes + // 7 fields, 4 bytes each reference (32bit) - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = AddressSerializer.assemble(kind, pubKeyHex, dTag) fun toATagArray() = assemble(toTag(), relay) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/dTag/DTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/dTag/DTag.kt index 2b4b3c16e..413b09005 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/dTag/DTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/dTag/DTag.kt @@ -20,14 +20,9 @@ */ package com.vitorpamplona.quartz.nip01Core.tags.dTag -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - class DTag( val dId: String, ) { - fun countMemory(): Int = 1 * pointerSizeInBytes + dId.bytesUsedInMemory() - fun toTagArray() = assemble(dId) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt index a11296052..d286581bb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/events/ETag.kt @@ -30,9 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ETag( @@ -46,12 +44,6 @@ data class ETag( this.author = authorPubKeyHex } - fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(eventId, author, null, relay) override fun toTagArray() = assemble(eventId, relay, author) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt index c827ff84d..d47153de5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/tags/people/PTag.kt @@ -33,20 +33,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class PTag( override val pubKey: HexKey, override val relayHint: NormalizedRelayUrl? = null, ) : PubKeyReferenceTag { - fun countMemory(): Int = - 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) - pubKey.bytesUsedInMemory() + - (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) fun toNPub(): String = pubKey.hexToByteArray().toNpub() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt index 8320fcfcf..107a96aa1 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip02FollowList/tags/ContactTag.kt @@ -31,9 +31,7 @@ import com.vitorpamplona.quartz.nip19Bech32.decodePublicKey import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ContactTag( @@ -51,12 +49,6 @@ data class ContactTag( this.petname = petname } - fun countMemory(): Int = - 3 * pointerSizeInBytes + - pubKey.bytesUsedInMemory() + - (relayUri?.url?.bytesUsedInMemory() ?: 0) + - (petname?.bytesUsedInMemory() ?: 0) - fun toTagArray() = assemble(pubKey, relayUri, petname) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt index b5a526efb..352386f02 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip13Pow/tags/PoWTag.kt @@ -22,16 +22,12 @@ package com.vitorpamplona.quartz.nip13Pow.tags import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class PoWTag( val nonce: String, val commitment: Int?, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + nonce.bytesUsedInMemory() + (commitment?.bytesUsedInMemory() ?: 0) - fun toTagArray() = assemble(nonce, commitment) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt index a84e27981..17ddadb8d 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QAddressableTag.kt @@ -27,9 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class QAddressableTag( @@ -53,11 +51,6 @@ data class QAddressableTag( this.relay = relayHint } - fun countMemory(): Int = - 2 * pointerSizeInBytes + - address.countMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) - override fun toTagArray() = assemble(address, relay) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt index 3f088ec42..b50f440ab 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip18Reposts/quotes/QEventTag.kt @@ -28,9 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class QEventTag( @@ -44,12 +42,6 @@ data class QEventTag( this.author = authorPubKeyHex } - fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - override fun toTagArray() = assemble(eventId, relay, author) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt index c2d386972..21c999bf5 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/list/tags/ChannelTag.kt @@ -28,9 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class ChannelTag( @@ -38,12 +36,6 @@ class ChannelTag( val relay: NormalizedRelayUrl? = null, val author: HexKey? = null, ) { - fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(eventId, author, null, relay) fun toTagArray() = assemble(eventId, relay, author) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerMessage.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerMessage.kt index 0b1921e92..bb19fd777 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerMessage.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerMessage.kt @@ -22,6 +22,4 @@ package com.vitorpamplona.quartz.nip46RemoteSigner import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable -abstract class BunkerMessage : OptimizedSerializable { - abstract fun countMemory(): Int -} +abstract class BunkerMessage : OptimizedSerializable diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequest.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequest.kt index acb102a1e..ee0f51405 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequest.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerRequest.kt @@ -20,17 +20,8 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - open class BunkerRequest( val id: String, val method: String, val params: Array = emptyArray(), -) : BunkerMessage() { - override fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - id.bytesUsedInMemory() + - method.bytesUsedInMemory() + - params.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } -} +) : BunkerMessage() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponse.kt index 61ab48443..5ff45c08c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponse.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/BunkerResponse.kt @@ -20,17 +20,8 @@ */ package com.vitorpamplona.quartz.nip46RemoteSigner -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - open class BunkerResponse( val id: String, val result: String?, val error: String?, -) : BunkerMessage() { - override fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - id.bytesUsedInMemory() + - (result?.bytesUsedInMemory() ?: 0) + - (error?.bytesUsedInMemory() ?: 0) -} +) : BunkerMessage() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt index 4dc25157a..91ae3715f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip47WalletConnect/Response.kt @@ -21,15 +21,11 @@ package com.vitorpamplona.quartz.nip47WalletConnect import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes // RESPONSE OBJECTS abstract class Response( val resultType: String, -) : OptimizedSerializable { - abstract fun countMemory(): Int -} +) : OptimizedSerializable // PayInvoice Call @@ -38,11 +34,7 @@ class PayInvoiceSuccessResponse( ) : Response("pay_invoice") { class PayInvoiceResultParams( val preimage: String? = null, - ) { - fun countMemory(): Int = pointerSizeInBytes + (preimage?.bytesUsedInMemory() ?: 0) - } - - override fun countMemory(): Int = pointerSizeInBytes + (result?.countMemory() ?: 0) + ) } class PayInvoiceErrorResponse( @@ -51,11 +43,7 @@ class PayInvoiceErrorResponse( class PayInvoiceErrorParams( val code: ErrorType? = null, val message: String? = null, - ) { - fun countMemory(): Int = pointerSizeInBytes + pointerSizeInBytes + (message?.bytesUsedInMemory() ?: 0) - } - - override fun countMemory(): Int = pointerSizeInBytes + (error?.countMemory() ?: 0) + ) enum class ErrorType { RATE_LIMITED, diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip48ProxyTags/ProxyTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip48ProxyTags/ProxyTag.kt index 5d41c61e3..d18a4e11c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip48ProxyTags/ProxyTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip48ProxyTags/ProxyTag.kt @@ -22,17 +22,13 @@ package com.vitorpamplona.quartz.nip48ProxyTags import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.has -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable data class ProxyTag( val id: String, val protocol: String, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + id.bytesUsedInMemory() + protocol.bytesUsedInMemory() - fun toTagArray() = assemble(id, protocol) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt index 92d11a598..eae9cf1c8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/AddressBookmark.kt @@ -27,16 +27,12 @@ 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.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class AddressBookmark( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) : BookmarkIdTag { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) override fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt index f81bcf2a3..e9d32a4ed 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/EventBookmark.kt @@ -28,9 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class EventBookmark( @@ -38,12 +36,6 @@ class EventBookmark( val relay: NormalizedRelayUrl? = null, val author: HexKey? = null, ) : BookmarkIdTag { - fun countMemory(): Int = - 3 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.url?.bytesUsedInMemory() ?: 0) + - (author?.bytesUsedInMemory() ?: 0) - fun toNEvent(): String = NEvent.create(eventId, author, null, relay) override fun toTagArray() = assemble(eventId, relay, author) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt index c67a1fdbf..44423095b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/UserTag.kt @@ -30,19 +30,12 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile import com.vitorpamplona.quartz.nip19Bech32.toNpub import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class UserTag( val pubKey: HexKey, val relayHint: NormalizedRelayUrl? = null, ) : MuteTag { - fun countMemory(): Int = - 2 * pointerSizeInBytes + // 2 fields, 4 bytes each reference (32bit) - pubKey.bytesUsedInMemory() + - (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toNProfile(): String = NProfile.create(pubKey, relayHint?.let { listOf(it) } ?: emptyList()) fun toNPub(): String = pubKey.hexToByteArray().toNpub() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt index cb87f97fa..1bf539536 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/WordTag.kt @@ -23,18 +23,12 @@ package com.vitorpamplona.quartz.nip51Lists.muteList.tags import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class WordTag( val word: String, ) : MuteTag { - fun countMemory(): Int = - 1 * pointerSizeInBytes + // 1 fields, 4 bytes each reference (32bit) - word.bytesUsedInMemory() - override fun toTagArray() = assemble(word) override fun toTagIdOnly() = assemble(word) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/tags/MeetingSpaceTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/tags/MeetingSpaceTag.kt index c43eb30d8..889ec5fd6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/tags/MeetingSpaceTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/meetingSpaces/tags/MeetingSpaceTag.kt @@ -27,16 +27,12 @@ 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.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class MeetingSpaceTag( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/presence/tags/MeetingRoomTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/presence/tags/MeetingRoomTag.kt index 1c487501b..5fd7cc690 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/presence/tags/MeetingRoomTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip53LiveActivities/presence/tags/MeetingRoomTag.kt @@ -27,16 +27,12 @@ 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.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class MeetingRoomTag( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt index d6962b031..24159c810 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/LnZapEvent.kt @@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.utils.Log -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class LnZapEvent( @@ -74,11 +73,6 @@ class LnZapEvent( } } - override fun countMemory(): Int = - super.countMemory() + - pointerSizeInBytes + (zapRequest?.countMemory() ?: 0) + // rough calculation - pointerSizeInBytes + 36 // bigdecimal size - override fun containedPost(): LnZapRequestEvent? = try { description()?.ifBlank { null }?.let { fromJson(it) } as? LnZapRequestEvent diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt index a666a41e4..40e5cffb7 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/zapraiser/ZapRaiserTag.kt @@ -20,14 +20,9 @@ */ package com.vitorpamplona.quartz.nip57Zaps.zapraiser -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - class ZapRaiserTag( val amountInSats: Long, ) { - fun countMemory(): Int = 1 * pointerSizeInBytes + amountInSats.bytesUsedInMemory() - fun toTagArray() = assemble(amountInSats) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt index c8a06bde8..ba354583b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/seals/SealedRumorEvent.kt @@ -30,8 +30,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class SealedRumorEvent( @@ -46,10 +44,6 @@ class SealedRumorEvent( @kotlin.jvm.Transient var innerEventId: HexKey? = null - override fun countMemory(): Int = - super.countMemory() + - pointerSizeInBytes + (innerEventId?.bytesUsedInMemory() ?: 0) - fun copyNoContent(): SealedRumorEvent { val copy = SealedRumorEvent( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt index de404e7a8..5b2a22e74 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip59Giftwrap/wraps/GiftWrapEvent.kt @@ -34,8 +34,6 @@ import com.vitorpamplona.quartz.nip59Giftwrap.HostStub import com.vitorpamplona.quartz.nip59Giftwrap.WrappedEvent import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class GiftWrapEvent( @@ -50,10 +48,6 @@ class GiftWrapEvent( @kotlin.jvm.Transient var innerEventId: HexKey? = null - override fun countMemory(): Int = - super.countMemory() + - pointerSizeInBytes + (innerEventId?.bytesUsedInMemory() ?: 0) - fun copyNoContent(): GiftWrapEvent { val copy = GiftWrapEvent( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt index 5de8e7452..5600e4fe0 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip71Video/tags/TextTrackTag.kt @@ -23,19 +23,12 @@ package com.vitorpamplona.quartz.nip71Video.tags import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.utils.arrayOfNotNull -import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes data class TextTrackTag( val eventId: HexKey, var relay: String? = null, ) { - fun countMemory(): Int = - 2 * pointerSizeInBytes + // 3 fields, 4 bytes each reference (32bit) - eventId.bytesUsedInMemory() + - (relay?.bytesUsedInMemory() ?: 0) - fun toTagArray() = arrayOfNotNull(TAG_NAME, eventId, relay) companion object { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/approval/tags/ApprovedAddressTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/approval/tags/ApprovedAddressTag.kt index f0b805183..9fb768adc 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/approval/tags/ApprovedAddressTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/approval/tags/ApprovedAddressTag.kt @@ -28,16 +28,12 @@ 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.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class ApprovedAddressTag( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt index fcdaba6ec..ee67f0b9e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip72ModCommunities/follow/tags/CommunityTag.kt @@ -28,16 +28,12 @@ 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.bytesUsedInMemory import com.vitorpamplona.quartz.utils.ensure -import com.vitorpamplona.quartz.utils.pointerSizeInBytes class CommunityTag( val address: Address, val relayHint: NormalizedRelayUrl? = null, ) { - fun countMemory(): Int = 2 * pointerSizeInBytes + address.countMemory() + (relayHint?.url?.bytesUsedInMemory() ?: 0) - fun toTag() = Address.assemble(address.kind, address.pubKeyHex, address.dTag) fun toTagArray() = assemble(address, relayHint) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt index a7706ff6e..0b5d2536e 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppDefinitionEvent.kt @@ -51,8 +51,6 @@ class AppDefinitionEvent( sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig), PublishedAtProvider { - override fun countMemory(): Int = super.countMemory() + (cachedMetadata?.countMemory() ?: 8) - @kotlinx.serialization.Transient @kotlin.jvm.Transient private var cachedMetadata: AppMetadata? = null diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt index 44b6d8bb8..8053473fd 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip89AppHandlers/definition/AppMetadata.kt @@ -22,8 +22,6 @@ package com.vitorpamplona.quartz.nip89AppHandlers.definition import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.nip01Core.core.JsonMapper -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -52,26 +50,6 @@ class AppMetadata { var lud06: String? = null var lud16: String? = null - fun countMemory(): Int = - 20 * pointerSizeInBytes + // 20 fields, 4 bytes for each reference - (name?.bytesUsedInMemory() ?: 0) + - (username?.bytesUsedInMemory() ?: 0) + - (displayName?.bytesUsedInMemory() ?: 0) + - (picture?.bytesUsedInMemory() ?: 0) + - (banner?.bytesUsedInMemory() ?: 0) + - (image?.bytesUsedInMemory() ?: 0) + - (website?.bytesUsedInMemory() ?: 0) + - (about?.bytesUsedInMemory() ?: 0) + - (subscription?.bytesUsedInMemory() ?: 0) + - (acceptsNutZaps?.bytesUsedInMemory() ?: 0) + - (supportsEncryption?.bytesUsedInMemory() ?: 0) + - (personalized?.bytesUsedInMemory() ?: 0) + // A Boolean has 8 bytes of header, plus 1 byte of payload, for a total of 9 bytes of information. The JVM then rounds it up to the next multiple of 8. so the one instance of java.lang.Boolean takes up 16 bytes of memory. - (amount?.bytesUsedInMemory() ?: 0) + - (nip05?.bytesUsedInMemory() ?: 0) + - (domain?.bytesUsedInMemory() ?: 0) + - (lud06?.bytesUsedInMemory() ?: 0) + - (lud16?.bytesUsedInMemory() ?: 0) - fun anyName(): String? = displayName ?: name ?: username fun anyNameStartsWith(prefix: String): Boolean = diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt index c4e1500b3..4dfad6cd8 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip90Dvms/NIP90ContentDiscoveryResponseEvent.kt @@ -28,8 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.TimeUtils -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes @Immutable class NIP90ContentDiscoveryResponseEvent( @@ -44,10 +42,6 @@ class NIP90ContentDiscoveryResponseEvent( @kotlin.jvm.Transient var events: List? = null - override fun countMemory(): Int = - super.countMemory() + - pointerSizeInBytes + (events?.sumOf { it.bytesUsedInMemory() } ?: 0) - fun innerTags(): List { if (content.isEmpty()) { return listOf() diff --git a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.ios.kt b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.ios.kt index 521c0dadd..d91d93bc9 100644 --- a/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.ios.kt +++ b/quartz/src/iosMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.ios.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.core -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - actual data class Address actual constructor( actual val kind: Kind, actual val pubKeyHex: HexKey, @@ -30,12 +27,6 @@ actual data class Address actual constructor( ) : Comparable
{ actual fun toValue() = assemble(kind, pubKeyHex, dTag) - actual fun countMemory(): Int = - 3 * pointerSizeInBytes + - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() - actual override fun compareTo(other: Address): Int { val result = kind.compareTo(other.kind) return if (result == 0) { diff --git a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.jvm.kt b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.jvm.kt index 521c0dadd..d91d93bc9 100644 --- a/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.jvm.kt +++ b/quartz/src/jvmMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Address.jvm.kt @@ -20,9 +20,6 @@ */ package com.vitorpamplona.quartz.nip01Core.core -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes - actual data class Address actual constructor( actual val kind: Kind, actual val pubKeyHex: HexKey, @@ -30,12 +27,6 @@ actual data class Address actual constructor( ) : Comparable
{ actual fun toValue() = assemble(kind, pubKeyHex, dTag) - actual fun countMemory(): Int = - 3 * pointerSizeInBytes + - 8 + // kind - pubKeyHex.bytesUsedInMemory() + - dTag.bytesUsedInMemory() - actual override fun compareTo(other: Address): Int { val result = kind.compareTo(other.kind) return if (result == 0) { From 208f57830fb6a591e89035ec7e9d770b886fd847 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 14 Jan 2026 22:56:07 +0000 Subject: [PATCH 56/70] New Crowdin translations by GitHub Action --- .../src/main/res/values-pl-rPL/strings.xml | 6 ++ .../src/main/res/values-sl-rSI/strings.xml | 60 ++++++++++++++++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml index 8d5434880..88b468e25 100644 --- a/amethyst/src/main/res/values-pl-rPL/strings.xml +++ b/amethyst/src/main/res/values-pl-rPL/strings.xml @@ -172,6 +172,12 @@ Nieoczekiwany status przesyłania NIP-95 nie jest jeszcze dostępny dla wiadomości głosowych Nie udało się przesłać głosu: %1$s + Brak + Niska + Wysoka + Neutralna + Anonimowy + Zmienia barwę głosu. Uwaga: podstawowe zmiany barwy głosu mogą zostać wykryte przez uważnych słuchaczy. Użytkownik nie ma skonfigurowanego adresu LN, aby odbierać satsy "odpowiedz tutaj.. " Kopiuje ID wpisu do schowka w celu udostępnienia w Nostr diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml index 86b75dcd2..d72e872f7 100644 --- a/amethyst/src/main/res/values-sl-rSI/strings.xml +++ b/amethyst/src/main/res/values-sl-rSI/strings.xml @@ -15,6 +15,8 @@ Dešifriranje sporočila ni uspelo Slika skupine Eksplicitna vsebina + Obvestilo releja + Podvojena objava Nezaželena vsebina Iz tega releja izvira številna nezaželena vsebina Oponašanje @@ -132,6 +134,7 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Naslov releja Pošlji Bajti + Napaka Napake Število napak pri povezovanju v tej seji Domače vsebine @@ -143,6 +146,8 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Dodaj Uporabnika Dodaj rele Ime + Ime (za @označbe) + Moje ime @oznake Prikazano ime Moje prikazno ime Janez Slovenc @@ -169,7 +174,21 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Posnemi sporočilo Posnemi sporočilo Klikni in drži za snemanje sporočila + Ponovno posnemi + Snemam + Snemam %1$s Nalaganje … + Naloži napako + Neuspešno nalaganje zvočnega sporočila + Nepričakovan status nalaganja + NIP-95 še ne podpira zvočnih posnetkov + Neuspešno nalaganje zvočnega posnetka: %1$s + Nič + Globok + Visok + Nevtralen + Anonimiziraj + Prilagodi višino tona svojega glasu: Opomba: osnovne spremembe višine tona lahko poslušalci potencialno razveljavijo. Uporabnik nima nastavljenega \"lightning\" naslova za sprejem satoshi-jev "odgovori tukaj.. " Kopira ID zapiska v odložišče za deljenje v Nostr @@ -363,6 +382,8 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Blokiraj Ročno razdeli zape Zaznamki + Privzeti zaznamki + Tvoje privzeti zaznamki, ki jih podpira veliko Nostr odjemalcev. Osnutki Privatni zaznamki Javni zaznamki @@ -646,28 +667,64 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Količina bajtov, ki je bila prejeta od tega releja, vključno s filtri in dogodki Prišlo je do napake pri pridobivanju informacij o releju iz %1$s Lastnik + Servisni ključ + V teku %1$s + V teku %1$s (%2$s) Različica Program Kontakt Podprti NIP-i + Podprti upravljalniki vsebine Vstopnina + Vstopnina + Naročnina + Publikacija + Plačila %1$s URL za plačila + Ciljna skupina + Pravilnik & Povezave + Provizije & Plačila Omejitve Države Jeziki Oznake + Teme + Vse države + Vsi jeziki Pravila objavljanja + Izjava o zasebnosti + Določila & Pogoji + N/A Napake in obvestila s tega releja Dolžina sporočila Naročnine Filtri Dolžina id naročnine Minimalna predpona - Maksimum oznak v dogodku + Največ oznak v dogodku Dolžina vsebine + Največja dolžina vsebine + Zavrži starejše od + Sprejme do + %1$s v prihodnost + Pred %1$s + %1$s ničel + %1$s bitov + Zadržanje dogodka + Velikost vsebine + Povezljivost + Nadzor dostopa Minimalen PoW Auth + Potrebna avtentikacija Plačilo + Potrebno je plačilo + Največja dolžina sporočila + Največ naročnin + Največ filtrov na naročnino + Največja omejitev (vrnjenih dogodkov) + Privzeta omejitev (vrnjenih dogodkov) + Največja dolžina SubID Cashu žeton Unovči Pošlji v Zap denarnico @@ -1193,4 +1250,5 @@ Za podpisovanje se je potrebno prijaviti s privatnim ključem Oddajaj paket Izbriši seznam Izbriši paket + Tipi From 5ce4391db4505a1e224bee3934469e1727e3c194 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 10:29:06 +0100 Subject: [PATCH 57/70] guard encoder / decoder .stop --- .../amethyst/ui/actions/uploads/VoiceAnonymizer.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 391b79349..c8742fdb8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -196,7 +196,11 @@ class VoiceAnonymizer { return DecodedAudio(pcmSamples.toFloatArray(), sampleRate, duration) } finally { - decoder?.stop() + try { + decoder?.stop() + } catch (_: IllegalStateException) { + // Decoder was never started + } decoder?.release() extractor.release() } @@ -400,7 +404,11 @@ class VoiceAnonymizer { } } } finally { - encoder.stop() + try { + encoder.stop() + } catch (_: IllegalStateException) { + // Encoder was never started + } encoder.release() if (muxerStarted) { muxer.stop() From c6d437b98e367ae5f65c4744a6b41b255415a45e Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 10:47:03 +0100 Subject: [PATCH 58/70] confirmed: <1 results in Higher pitch. Adjusted NEUTRAL to slightly lower pitch --- .../amethyst/ui/actions/uploads/VoiceAnonymizationController.kt | 1 + .../vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt index b720e20d8..33adff8b6 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -61,6 +61,7 @@ class VoiceAnonymizationController( preset: VoicePreset, originalFile: File?, ) { + Log.d(logTag, "selectPreset called with: ${preset.name}, pitchFactor: ${preset.pitchFactor}") if (processingPreset != null || preset == selectedPreset) return if (preset == VoicePreset.NONE) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt index 49544e51a..b6c67d244 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoicePreset.kt @@ -29,5 +29,5 @@ enum class VoicePreset( NONE(1.0, R.string.voice_preset_none), DEEP(1.4, R.string.voice_preset_deep), HIGH(0.75, R.string.voice_preset_high), - NEUTRAL(0.9, R.string.voice_preset_neutral), + NEUTRAL(1.1, R.string.voice_preset_neutral), } From 708c6da9fe85afabe1c2aac6822d38aa3646f0ab Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 10:52:58 +0100 Subject: [PATCH 59/70] add docs --- .../ui/actions/uploads/VoiceAnonymizer.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index c8742fdb8..7240c87ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -41,12 +41,26 @@ import java.io.File import java.nio.ByteOrder import kotlin.math.abs +/** + * Result of voice anonymization processing. + * + * @property file The output audio file (AAC in MP4 container) + * @property waveform Amplitude data for waveform visualization (one value per second) + * @property duration Audio duration in seconds + */ data class AnonymizedResult( val file: File, val waveform: List, val duration: Int, ) +/** + * Processes audio files to alter voice characteristics for privacy. + * + * Uses TarsosDSP's WSOLA (Waveform Similarity Overlap-Add) algorithm combined with + * rate transposition to shift pitch while preserving duration. Note that in TarsosDSP, + * pitch factors work inversely: factor < 1 raises pitch, factor > 1 lowers pitch. + */ class VoiceAnonymizer { companion object { private const val TAG = "VoiceAnonymizer" @@ -54,6 +68,20 @@ class VoiceAnonymizer { private const val BIT_RATE = 128000 } + /** + * Applies voice anonymization to an audio file. + * + * The process involves three stages: + * 1. Decode input audio to PCM (0-30% progress) + * 2. Apply pitch shifting with TarsosDSP (30-70% progress) + * 3. Encode processed audio to AAC (70-100% progress) + * + * @param inputFile Source audio file (supports formats decodable by MediaCodec) + * @param preset Voice transformation preset (NONE is not allowed) + * @param onProgress Callback invoked with progress value from 0.0 to 1.0 + * @return [Result.success] with [AnonymizedResult] containing the output file, + * waveform data, and duration; or [Result.failure] with the exception + */ suspend fun anonymize( inputFile: File, preset: VoicePreset, From 001dce8fef74644d8fbacbb46cb024743565817b Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 10:56:28 +0100 Subject: [PATCH 60/70] guard for null inputFile.parentFile --- .../amethyst/ui/actions/uploads/VoiceAnonymizer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 7240c87ea..db831a2ae 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -126,7 +126,8 @@ class VoiceAnonymizer { ): File { val baseName = inputFile.nameWithoutExtension val presetSuffix = preset.name.lowercase() - return File(inputFile.parentFile, "${baseName}_$presetSuffix.mp4") + val parentDir = inputFile.parentFile ?: inputFile.absoluteFile.parentFile + return File(parentDir, "${baseName}_$presetSuffix.mp4") } private data class DecodedAudio( From 0b233f3a3f4a347dcade1183337d374e9b11496d Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 11:00:42 +0100 Subject: [PATCH 61/70] use colorScheme.onSurfaceVariant instead of Gray --- .../amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt index 3b7f02028..ad4cce69d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationSection.kt @@ -30,7 +30,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R @@ -63,7 +62,7 @@ fun VoiceAnonymizationSection( Text( text = stringRes(R.string.voice_anonymize_description), style = MaterialTheme.typography.bodySmall, - color = Color.Gray, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 3, overflow = TextOverflow.Ellipsis, ) From 50b0e8bd17e90c21056fadc8859496bf220c8a17 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 11:33:30 +0100 Subject: [PATCH 62/70] Moved processingPreset = preset to execute synchronously before launching the coroutine --- .../amethyst/ui/actions/uploads/VoiceAnonymizationController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt index 33adff8b6..fdb54523e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -77,9 +77,9 @@ class VoiceAnonymizationController( val file = originalFile ?: return processingJob?.cancel() + processingPreset = preset processingJob = scope.launch { - processingPreset = preset try { val anonymizer = VoiceAnonymizer() val result = anonymizer.anonymize(file, preset) From db75a7c910c030fc15bd749ac11c9d8bcc3eeb98 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 11:40:38 +0100 Subject: [PATCH 63/70] preallocate list capacity --- .../amethyst/ui/actions/uploads/VoiceAnonymizer.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index db831a2ae..71838a176 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -172,7 +172,8 @@ class VoiceAnonymizer { decoder.configure(format, null, null, 0) decoder.start() - val pcmSamples = mutableListOf() + val estimatedSamples = (sampleRate.toLong() * durationUs / 1_000_000).toInt() + val pcmSamples = ArrayList(estimatedSamples) val bufferInfo = MediaCodec.BufferInfo() var inputDone = false var outputDone = false @@ -251,8 +252,8 @@ class VoiceAnonymizer { } else -> baseFactor } - val processedSamples = mutableListOf() val totalSamples = pcmData.size + val processedSamples = ArrayList(totalSamples) val wsola = WaveformSimilarityBasedOverlapAdd( From 5be16338438cfdd61a2bda46192006d3d012a3fd Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 12:59:25 +0100 Subject: [PATCH 64/70] limit voice recording to 180s --- .../amethyst/ui/actions/uploads/RecordAudio.kt | 7 +++++++ .../amethyst/ui/actions/uploads/RecordVoiceButton.kt | 6 +++++- .../com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt | 2 ++ .../ui/screen/loggedIn/home/ShortNotePostScreen.kt | 2 ++ .../amethyst/ui/screen/loggedIn/home/VoiceReplyScreen.kt | 2 ++ 5 files changed, 18 insertions(+), 1 deletion(-) 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 32af08e0b..c7fe883aa 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 @@ -42,11 +42,14 @@ import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.delay import kotlinx.coroutines.isActive +const val MAX_VOICE_RECORD_SECONDS = 180 + @OptIn(ExperimentalPermissionsApi::class) @Composable fun RecordAudioBox( modifier: Modifier, onRecordTaken: (RecordingResult) -> Unit, + maxDurationSeconds: Int? = null, content: @Composable (Boolean, Int) -> Unit, ) { val mediaRecorder = remember { mutableStateOf(null) } @@ -107,6 +110,10 @@ fun RecordAudioBox( while (isActive) { delay(1000) elapsedSeconds++ + if (maxDurationSeconds != null && elapsedSeconds >= maxDurationSeconds) { + stopRecording() + break + } } } else { // Reset elapsed time when not recording diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt index 9cedeeab6..5d9fd487f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/RecordVoiceButton.kt @@ -42,7 +42,10 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.stringRes @Composable -fun RecordVoiceButton(onVoiceTaken: (RecordingResult) -> Unit) { +fun RecordVoiceButton( + onVoiceTaken: (RecordingResult) -> Unit, + maxDurationSeconds: Int? = null, +) { var isRecording by remember { mutableStateOf(false) } var elapsedSeconds by remember { mutableIntStateOf(0) } @@ -61,6 +64,7 @@ fun RecordVoiceButton(onVoiceTaken: (RecordingResult) -> Unit) { elapsedSeconds = 0 onVoiceTaken(recording) }, + maxDurationSeconds = maxDurationSeconds, ) { recordingState, elapsed -> // Update parent state after composition completes SideEffect { 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 a2814a464..ffcb0a2d4 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 @@ -113,6 +113,7 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNo import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCFinderFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled import com.vitorpamplona.amethyst.ui.actions.uploads.FloatingRecordingIndicator +import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.components.AnimatedBorderTextCornerRadius import com.vitorpamplona.amethyst.ui.components.ClickableBox @@ -635,6 +636,7 @@ fun ReplyViaVoiceReaction( ) } }, + maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, ) { isRecording, elapsedSeconds -> if (voiceRecordingState != null) { SideEffect { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 4318f13c8..e61651c12 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType +import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordVoiceButton import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia @@ -497,6 +498,7 @@ private fun BottomRowActions(postViewModel: ShortNotePostViewModel) { onVoiceTaken = { recording -> postViewModel.selectVoiceRecording(recording) }, + maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, ) if (postViewModel.canUsePoll) { 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 a2573663b..7eeac6f77 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 @@ -51,6 +51,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow +import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection @@ -226,6 +227,7 @@ private fun ReRecordButton(viewModel: VoiceReplyViewModel) { onRecordTaken = { recording -> viewModel.selectRecording(recording) }, + maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, ) { isRecording, elapsedSeconds -> val contentColor = if (isRecording) { From 76a05f7d71e3efe42f4861bdc5113a8bc5caddf7 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 13:56:49 +0100 Subject: [PATCH 65/70] moved re-record button inline and added to VoiceMessagePreview.kt --- .../ui/actions/uploads/VoiceMessagePreview.kt | 202 +++++++++++++----- .../loggedIn/home/ShortNotePostScreen.kt | 2 + .../screen/loggedIn/home/VoiceReplyScreen.kt | 98 +-------- 3 files changed, 148 insertions(+), 154 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt index 7633a47d8..40f60124e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceMessagePreview.kt @@ -35,8 +35,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Mic import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -67,6 +69,8 @@ fun VoiceMessagePreview( voiceMetadata: AudioMeta, localFile: File? = null, onRemove: () -> Unit, + onReRecord: ((RecordingResult) -> Unit)? = null, + isUploading: Boolean = false, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -101,77 +105,159 @@ fun VoiceMessagePreview( shape = RoundedCornerShape(8.dp), ).padding(12.dp), ) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - // Play/Pause Button - IconButton( - onClick = { - handlePlayPauseClick( - mediaPlayer = mediaPlayer, - isPlaying = isPlaying, - progress = progress, - onProgressReset = { progress = 0f }, - onPlayingChanged = { isPlaying = it }, - ) - }, - modifier = Modifier.size(48.dp), + Column { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - Icon( - imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, - contentDescription = if (isPlaying) stringRes(context, R.string.pause) else stringRes(context, R.string.play), - tint = MaterialTheme.colorScheme.primary, - ) - } - - Spacer(modifier = Modifier.width(8.dp)) - - // Waveform and Duration - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center, - ) { - AudioWaveformReadOnly( - amplitudes = voiceMetadata.waveform ?: emptyList(), - progress = progress, - waveformBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant)), - progressBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.primary)), - onProgressChange = { newProgress -> - handleWaveformScrub( - newProgress = newProgress, + // Play/Pause Button + IconButton( + onClick = { + handlePlayPauseClick( mediaPlayer = mediaPlayer, - onProgressChanged = { progress = it }, + isPlaying = isPlaying, + progress = progress, + onProgressReset = { progress = 0f }, + onPlayingChanged = { isPlaying = it }, ) }, - ) + modifier = Modifier.size(48.dp), + ) { + Icon( + imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, + contentDescription = if (isPlaying) stringRes(context, R.string.pause) else stringRes(context, R.string.play), + tint = MaterialTheme.colorScheme.primary, + ) + } - Text( - text = formatSecondsToTime(voiceMetadata.duration ?: 0), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 4.dp), - ) + Spacer(modifier = Modifier.width(8.dp)) + + // Waveform and Duration + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center, + ) { + AudioWaveformReadOnly( + amplitudes = voiceMetadata.waveform ?: emptyList(), + progress = progress, + waveformBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.onSurfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant)), + progressBrush = Brush.linearGradient(listOf(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.primary)), + onProgressChange = { newProgress -> + handleWaveformScrub( + newProgress = newProgress, + mediaPlayer = mediaPlayer, + onProgressChanged = { progress = it }, + ) + }, + ) + + Text( + text = formatSecondsToTime(voiceMetadata.duration ?: 0), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp), + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + // Remove Button + IconButton( + onClick = onRemove, + modifier = Modifier.size(48.dp), + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringRes(context, R.string.remove), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - Spacer(modifier = Modifier.width(8.dp)) - - // Remove Button - IconButton( - onClick = onRemove, - modifier = Modifier.size(48.dp), - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringRes(context, R.string.remove), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + if (onReRecord != null) { + Spacer(modifier = Modifier.size(8.dp)) + ReRecordButton( + isUploading = isUploading, + isPlaying = isPlaying, + onRecordTaken = onReRecord, ) } } } } +@Composable +private fun ReRecordButton( + isUploading: Boolean, + isPlaying: Boolean, + onRecordTaken: (RecordingResult) -> Unit, +) { + if (isUploading || isPlaying) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Mic, + contentDescription = stringRes(id = R.string.record_a_message), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = stringRes(id = R.string.re_record), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + return + } + + RecordAudioBox( + modifier = Modifier, + onRecordTaken = onRecordTaken, + maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, + ) { isRecording, elapsedSeconds -> + val contentColor = + if (isRecording) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + } + 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 = icon, + contentDescription = iconDescription, + tint = contentColor, + ) + Text( + text = label, + color = contentColor, + ) + } + } +} + @Composable private fun ManageMediaPlayer( voiceMetadata: AudioMeta, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index e61651c12..99f6d42e0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -375,6 +375,8 @@ private fun NewPostScreenBody( VoiceMessagePreview( voiceMetadata = displayMetadata, localFile = postViewModel.activeFile, + onReRecord = { recording -> postViewModel.selectVoiceRecording(recording) }, + isUploading = postViewModel.isUploadingVoice, onRemove = { postViewModel.removeVoiceMessage() }, ) 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 7eeac6f77..5baff1a94 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 @@ -21,47 +21,32 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding 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 import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow -import com.vitorpamplona.amethyst.ui.actions.uploads.MAX_VOICE_RECORD_SECONDS -import com.vitorpamplona.amethyst.ui.actions.uploads.RecordAudioBox import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection 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 import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.replyModifier @@ -104,9 +89,6 @@ fun VoiceReplyScreen( }, ) }, - bottomBar = { - ReRecordButton(viewModel) - }, ) { pad -> Surface( modifier = @@ -162,6 +144,8 @@ private fun VoiceReplyScreenBody( VoiceMessagePreview( voiceMetadata = displayMetadata, localFile = viewModel.activeFile, + onReRecord = { recording -> viewModel.selectRecording(recording) }, + isUploading = viewModel.isUploading, onRemove = { viewModel.cancel() nav.popBack() @@ -193,81 +177,3 @@ private fun VoiceReplyScreenBody( Spacer(modifier = Modifier.height(80.dp)) } } - -@Composable -private fun ReRecordButton(viewModel: VoiceReplyViewModel) { - Column( - modifier = - Modifier - .fillMaxWidth() - .navigationBarsPadding() - .padding(vertical = 16.dp, horizontal = Size10dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (viewModel.isUploading) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - imageVector = Icons.Default.Mic, - contentDescription = stringRes(id = R.string.record_a_message), - tint = MaterialTheme.colorScheme.onBackground, - ) - Text( - text = stringRes(id = R.string.re_record), - color = MaterialTheme.colorScheme.onBackground, - ) - } - return - } - - RecordAudioBox( - modifier = Modifier, - onRecordTaken = { recording -> - viewModel.selectRecording(recording) - }, - maxDurationSeconds = MAX_VOICE_RECORD_SECONDS, - ) { 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 = icon, - contentDescription = iconDescription, - tint = contentColor, - ) - Text( - text = label, - color = contentColor, - ) - } - } - } -} From 835fbb90b7da16175aa91803c3aeccd373e42097 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 14:00:13 +0100 Subject: [PATCH 66/70] Do something with the "Boolean" value returned by "delete". --- .../ui/actions/uploads/VoiceAnonymizationController.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt index fdb54523e..e154f0a27 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizationController.kt @@ -109,8 +109,11 @@ class VoiceAnonymizationController( distortedFiles.values.forEach { result -> try { if (result.file.exists()) { - result.file.delete() - Log.d(logTag, "Deleted distorted file: ${result.file.absolutePath}") + if (result.file.delete()) { + Log.d(logTag, "Deleted distorted file: ${result.file.absolutePath}") + } else { + Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}") + } } } catch (e: Exception) { Log.w(logTag, "Failed to delete distorted file: ${result.file.absolutePath}", e) From 40d694343909b760b77a9a562878c50631525e55 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 14:04:59 +0100 Subject: [PATCH 67/70] sonar fixes --- .../ui/actions/uploads/VoiceAnonymizer.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt index 71838a176..8c4210bc4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/VoiceAnonymizer.kt @@ -158,9 +158,7 @@ class VoiceAnonymizer { } } - if (audioTrackIndex == -1 || format == null) { - throw IllegalStateException("No audio track found in file") - } + check(audioTrackIndex != -1 && format != null) { "No audio track found in file" } extractor.selectTrack(audioTrackIndex) val mime = format.getString(MediaFormat.KEY_MIME) ?: "audio/mp4a-latm" @@ -286,7 +284,9 @@ class VoiceAnonymizer { return true } - override fun processingFinished() {} + override fun processingFinished() { + // No-op: no cleanup needed + } } val dispatcher = @@ -310,7 +310,9 @@ class VoiceAnonymizer { return true } - override fun processingFinished() {} + override fun processingFinished() { + // No-op: no cleanup needed + } } dispatcher.addAudioProcessor(progressProcessor) @@ -484,5 +486,7 @@ private class FloatArrayAudioInputStream( return actualSkip.toLong() * 2 } - override fun close() {} + override fun close() { + // No-op: no cleanup needed + } } From 505a6583ee32dec83a146f6f79c42bedda325952 Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 15 Jan 2026 14:20:05 +0100 Subject: [PATCH 68/70] remove unused imports --- .../main/java/com/vitorpamplona/amethyst/ui/note/types/Nip.kt | 2 -- .../amethyst/commons/model/privateChats/Chatroom.kt | 1 - .../com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt | 1 - .../vitorpamplona/amethyst/commons/richtext/RichTextParser.kt | 1 - .../amethyst/desktop/network/DesktopRelayConnectionManager.kt | 1 - .../kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt | 1 - 6 files changed, 7 deletions(-) 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 index 557e26c46..2b4bb2fe2 100644 --- 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 @@ -20,8 +20,6 @@ */ 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 diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt index 5485bf5bf..dbf99854d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/model/privateChats/Chatroom.kt @@ -34,7 +34,6 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import java.lang.ref.WeakReference -import kotlin.collections.plus @Stable class Chatroom : NotesGatherer { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt index b588ea572..428428bce 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Base64Image.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.richtext -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser import java.util.Base64 object Base64Image { diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 70d90061d..b5dcf22fc 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -45,7 +45,6 @@ import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL import kotlin.coroutines.cancellation.CancellationException -import kotlin.text.iterator class RichTextParser { fun createMediaContent( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt index 18e7a9873..1b0a019ed 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/network/DesktopRelayConnectionManager.kt @@ -20,7 +20,6 @@ */ package com.vitorpamplona.amethyst.desktop.network -import com.vitorpamplona.amethyst.desktop.network.RelayConnectionManager import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket /** 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 b8ac14da8..018385c6b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip10Notes/TextNoteEvent.kt @@ -49,7 +49,6 @@ 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( From c801e9ce8678321f893e7ac0c305596da03e5eba Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 20 Jan 2026 13:34:58 +0000 Subject: [PATCH 69/70] New Crowdin translations by GitHub Action --- .../src/main/res/values-fr-rFR/strings.xml | 64 +++++++++++++++++++ .../src/main/res/values-hi-rIN/strings.xml | 7 ++ .../src/main/res/values-hu-rHU/strings.xml | 6 ++ .../src/main/res/values-zh-rCN/strings.xml | 7 ++ 4 files changed, 84 insertions(+) diff --git a/amethyst/src/main/res/values-fr-rFR/strings.xml b/amethyst/src/main/res/values-fr-rFR/strings.xml index bbdb00c1e..5ca190f7b 100644 --- a/amethyst/src/main/res/values-fr-rFR/strings.xml +++ b/amethyst/src/main/res/values-fr-rFR/strings.xml @@ -135,6 +135,8 @@ Ajouter un utilisateur Ajouter un relais Nom + Nom (pour @tagging) + Mon nom @tag Nom visible du public Mon nom visible du public Ostrich McGénial @@ -170,6 +172,12 @@ Erreur lors de l\'envoi NIP-95 n\'est pas encore supporté pour les messages vocaux Échec de l\'envoi de la voix : %1$s + Aucun + Profond + Haute + Neutre + Anonymisation + Modifie votre niveau de voix. Remarque : les changements de hauteur de base peuvent potentiellement être inversés par des auditeurs déterminés. L\'utilisateur n\'a pas configuré d\'adresse Lightning pour recevoir des sats "répondre ici" Copie l\'ID de note dans le presse-papiers pour le partage sur Nostr @@ -364,6 +372,7 @@ Division manuelle de zaps Favoris Signets par défaut + Vos favoris par défaut que de nombreux clients prennent en charge Brouillons Favoris Privés Favoris Publics @@ -371,6 +380,40 @@ Ajouter aux Favoris Publics Supprimer des Favoris Privés Supprimer des Favoris Publics + Listes de favoris + Icône pour la liste de favoris + Nouvelle liste de favoris + Métadonnées de la liste de favoris + Dupliquer la liste de favoris + Diffuser la liste de favoris + Supprimer la liste de favoris + Voir les publications + Voir les articles + Voir les liens + Voir les hashtags + Vous n\'avez pas encore de liste de favoris. Appuyez sur le bouton ci-dessous pour en créer une. + Publications privées + Publications privées (%1$s) + Publications publiques + Publications publiques (%1$s) + Articles privés + Articles privés (%1$s) + Articles publics + Articles publics (%1$s) + Gérer %1$s dans les listes de favoris + Ajouter cette publication aux favoris + Ajouter cet article aux favoris + est un favori public ici + est un favori privé ici + n\'est pas un favori ici + Supprimer le favori de la liste + Ajouter un favori à la liste + Ajouter comme favori public + Ajouter comme favori privé + Supprimer de la liste de favoris + Les métadonnées des listes de favoris peuvent être vues par n\'importe qui sur Nostr. Seuls vos membres privés sont chiffrés. + Déplacer en public + Déplacer en privé Service Wallet Connect Autorise un Nostr Secret à payer des zaps sans quitter l\'application. Gardez le secret en toute sécurité et utilisez un relais privé si possible Clé publique Wallet Connect @@ -604,12 +647,23 @@ Contact NIPs supportés Frais d\'admission + Publication + Paiements %1$s URL de payments + Audience cible + Politiques & liens + Frais & paiements Limites Pays Langages Étiquettes + Sujets + Tous les pays + Toutes les langues Politique de publication + Politique de confidentialité + Termes & Conditions + N/A Erreurs et Notifications de ce Relais Longueur du message Abonnements @@ -618,9 +672,19 @@ Préfixe minimum Tags d\'événement maximum Longueur du contenu + Longueur maximale du contenu + Rejeter plus anciens que + Accepte jusqu\'à + %1$s dans le futur + Il y a %1$s + Taille du contenu + Connectivité + Contrôle d\'accès PoW minimum Authentification + Authentification requise Paiement + Paiement requis Jeton Cashu Échanger Envoyer vers le portefeuille Zap diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml index 8fff23237..eeb0dfa93 100644 --- a/amethyst/src/main/res/values-hi-rIN/strings.xml +++ b/amethyst/src/main/res/values-hi-rIN/strings.xml @@ -172,6 +172,12 @@ अनपेक्षित आरोहण स्थिति निप॰९५ का आलम्बन अभी नहीं है ध्वनि सन्देशों के लिए ध्वनि आरोहण असफल : %1$s + कुछ भी नहीं + गहन + उच्च + मध्यम + अनामीकरण + आपके स्वर का परिवर्तन करता है। ध्यातव्य। आधारभूत स्वर परिवर्तन को पूर्ववत किया जा सकता है दृढ निश्चित श्रोताओं द्वारा। उपयोगकर्ता का कोई लैटनिंग पता स्थापित नहीं जिसपर साट्स प्राप्त कर सके "उत्तर यहाँ दें.. " नोस्ट्र में बाँटने के लिए टीका विभेदक की अनुकृति करता है टाँकाफलक में @@ -1232,4 +1238,5 @@ पोटली प्रसारण सूची मिटाएँ पोटली मिटाएँ + प्रकार diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml index d6716c99c..aa9ccf9d0 100644 --- a/amethyst/src/main/res/values-hu-rHU/strings.xml +++ b/amethyst/src/main/res/values-hu-rHU/strings.xml @@ -172,6 +172,12 @@ Váratlan feltöltési állapot A NIP-95 még nem támogatja a hangüzeneteket Nem sikerült feltölteni a hangüzenetet: %1$s + Semmi + Mély + Magas + Semleges + Névtelenítés + Módosítja az Ön hangmagasságát. Megjegyzés: az alapvető hangmagasság-változásokat a figyelmes hallgatók visszafordíthatják. A felhasználó nem rendelkezik a satoshik fogadásához beállított lightning-címmel "Válasz írása… " A bejegyzés azonosítóját a vágólapra másolja a Nostr-ban való megosztáshoz diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml index 6cbc2e7d5..7f427f586 100644 --- a/amethyst/src/main/res/values-zh-rCN/strings.xml +++ b/amethyst/src/main/res/values-zh-rCN/strings.xml @@ -172,6 +172,12 @@ 未知的上传状态 语音消息尚不支持 NIP-95 语音上传失败:%1$s + + 深沉 + 高音 + 中性 + 匿名化 + 更改您的音高。注意:听众如果下定决定也许能逆转基础音高更改。 用户尚未设置闪电地址以接收聪 "🔏在此回复… " 复制笔记ID到剪贴板,供分享 @@ -1232,4 +1238,5 @@ 广播包 删除列表 删除包 + 类型 From 29faa7882d8270e8a7229ac7eacffecdaa267ffc Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Tue, 20 Jan 2026 18:27:20 +0000 Subject: [PATCH 70/70] Increase max voice record duration to 600 seconds --- .../vitorpamplona/amethyst/ui/actions/uploads/RecordAudio.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c7fe883aa..7fe4340d8 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 @@ -42,7 +42,7 @@ import com.vitorpamplona.amethyst.ui.stringRes import kotlinx.coroutines.delay import kotlinx.coroutines.isActive -const val MAX_VOICE_RECORD_SECONDS = 180 +const val MAX_VOICE_RECORD_SECONDS = 600 @OptIn(ExperimentalPermissionsApi::class) @Composable