diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index b8414fe03..4c9e78d2f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1514,9 +1514,9 @@ class Account( private fun isAcceptableDirect(note: Note): Boolean { if (!warnAboutPostsWithReports) { - return note.reportsBy(userProfile()).isEmpty() + return !note.hasReportsBy(userProfile()) } - return note.reportsBy(userProfile()).isEmpty() && // if user has not reported this post + return !note.hasReportsBy(userProfile()) && // if user has not reported this post note.countReportAuthorsBy(followingKeySet()) < 5 // if it has 5 reports by reliable users } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index d09483d37..ac7e3fef3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -51,20 +51,20 @@ open class Note(val idHex: String) { var replyTo: List? = null // These fields are updated every time an event related to this note is received. - var replies = setOf() + var replies = listOf() private set - var reactions = mapOf>() + var reactions = mapOf>() private set - var boosts = setOf() + var boosts = listOf() private set - var reports = mapOf>() + var reports = mapOf>() private set var zaps = mapOf() private set var zapPayments = mapOf() private set - var relays = setOf() + var relays = listOf() private set var lastReactionsDownloadTime: Map = emptyMap() @@ -151,15 +151,20 @@ open class Note(val idHex: String) { } fun removeReply(note: Note) { - replies = replies - note - liveSet?.replies?.invalidateData() - } - fun removeBoost(note: Note) { - boosts = boosts - note - liveSet?.boosts?.invalidateData() + if (note in replies) { + replies = replies - note + liveSet?.replies?.invalidateData() + } } - fun removeAllChildNotes(): Set { + fun removeBoost(note: Note) { + if (note in boosts) { + boosts = boosts - note + liveSet?.boosts?.invalidateData() + } + } + + fun removeAllChildNotes(): List { val toBeRemoved = replies + reactions.values.flatten() + boosts + @@ -169,13 +174,13 @@ open class Note(val idHex: String) { zapPayments.keys + zapPayments.values.filterNotNull() - replies = setOf() - reactions = mapOf>() - boosts = setOf() - reports = mapOf>() + replies = listOf() + reactions = mapOf>() + boosts = listOf() + reports = mapOf>() zaps = mapOf() zapPayments = mapOf() - relays = setOf() + relays = listOf() lastReactionsDownloadTime = emptyMap() liveSet?.replies?.invalidateData() @@ -193,14 +198,16 @@ open class Note(val idHex: String) { if (reaction in reactions.keys && reactions[reaction]?.contains(note) == true) { reactions[reaction]?.let { - val newList = it.minus(note) - if (newList.isEmpty()) { - reactions = reactions.minus(reaction) - } else { - reactions = reactions + Pair(reaction, newList) - } + if (note in it) { + val newList = it.minus(note) + if (newList.isEmpty()) { + reactions = reactions.minus(reaction) + } else { + reactions = reactions + Pair(reaction, newList) + } - liveSet?.reactions?.invalidateData() + liveSet?.reactions?.invalidateData() + } } } } @@ -306,7 +313,7 @@ open class Note(val idHex: String) { val reaction = note.event?.content()?.firstFullCharOrEmoji(ImmutableListOfLists(tags)) ?: "+" if (reaction !in reactions.keys) { - reactions = reactions + Pair(reaction, setOf(note)) + reactions = reactions + Pair(reaction, listOf(note)) liveSet?.reactions?.invalidateData() } else if (reactions[reaction]?.contains(note) == false) { reactions = reactions + Pair(reaction, (reactions[reaction] ?: emptySet()) + note) @@ -318,7 +325,7 @@ open class Note(val idHex: String) { val author = note.author ?: return if (author !in reports.keys) { - reports = reports + Pair(author, setOf(note)) + reports = reports + Pair(author, listOf(note)) liveSet?.reports?.invalidateData() } else if (reports[author]?.contains(note) == false) { reports = reports + Pair(author, (reports[author] ?: emptySet()) + note) @@ -411,8 +418,8 @@ open class Note(val idHex: String) { return boosts.any { it.author?.pubkeyHex == user.pubkeyHex } } - fun reportsBy(user: User): Set { - return reports[user] ?: emptySet() + fun hasReportsBy(user: User): Boolean { + return reports[user]?.isNotEmpty() ?: false } fun reportAuthorsBy(users: Set): List { @@ -576,9 +583,9 @@ open class Note(val idHex: String) { } replyTo = null - replies = emptySet() + replies = emptyList() reactions = emptyMap() - boosts = emptySet() + boosts = emptyList() reports = emptyMap() zaps = emptyMap() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 6d1d38932..afa9705b2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.model.TimeUtils import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.CryptoUtils import com.vitorpamplona.amethyst.service.nip19.Nip19 +import com.vitorpamplona.amethyst.service.relays.bytesUsedInMemory import fr.acinq.secp256k1.Hex import java.lang.reflect.Type import java.math.BigDecimal @@ -24,6 +25,16 @@ open class Event( val content: String, val sig: HexKey ) : EventInterface { + + override fun countMemory(): Long { + return 12L + + id.bytesUsedInMemory() + + pubKey.bytesUsedInMemory() + + tags.sumOf { it.sumOf { it.bytesUsedInMemory() } } + + content.bytesUsedInMemory() + + sig.bytesUsedInMemory() + } + override fun id(): HexKey = id override fun pubKey(): HexKey = pubKey diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt index 840a59773..f548f60b4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt @@ -6,6 +6,8 @@ import java.math.BigDecimal @Immutable interface EventInterface { + fun countMemory(): Long + fun id(): HexKey fun pubKey(): HexKey diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 85d688c6d..bf51126e9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -56,9 +56,6 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val uri = intent?.data?.toString() - val startingPage = uriToRoute(uri) - LocalPreferences.migrateSingleUserPrefs() val language = LocalPreferences.getPreferredLanguage() if (language.isNotBlank()) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt index 0879cb365..a360bd35c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RobohashAsyncImage.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.components +import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -64,19 +65,30 @@ fun RobohashFallbackAsyncImage( model = Robohash.imageRequest(context, robot) ) - AsyncImage( - model = model, - contentDescription = contentDescription, - modifier = modifier, - placeholder = painter, - fallback = painter, - error = painter, - alignment = alignment, - contentScale = contentScale, - alpha = alpha, - colorFilter = colorFilter, - filterQuality = filterQuality - ) + if (model != null) { + AsyncImage( + model = model, + contentDescription = contentDescription, + modifier = modifier, + placeholder = painter, + fallback = painter, + error = painter, + alignment = alignment, + contentScale = contentScale, + alpha = alpha, + colorFilter = colorFilter, + filterQuality = filterQuality + ) + } else { + Image( + painter = painter, + contentDescription = contentDescription, + modifier = modifier, + alignment = alignment, + contentScale = contentScale, + colorFilter = colorFilter + ) + } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 13bc33732..4bbf9a4f6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -65,7 +65,6 @@ fun AppNavigation( navController: NavHostController, accountViewModel: AccountViewModel, - startingPage: String? = null, themeViewModel: ThemeViewModel ) { val scope = rememberCoroutineScope() @@ -250,7 +249,10 @@ fun AppNavigation( } } - var actionableNextPage by remember { mutableStateOf(startingPage) } + val activity = LocalContext.current.getActivity() + var actionableNextPage by remember { + mutableStateOf(uriToRoute(activity.intent?.data?.toString()?.ifBlank { null })) + } actionableNextPage?.let { LaunchedEffect(it) { navController.navigate(it) { @@ -261,7 +263,6 @@ fun AppNavigation( actionableNextPage = null } - val activity = LocalContext.current.getActivity() DisposableEffect(navController) { val consumer = Consumer { intent -> val uri = intent?.data?.toString() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 9389d7164..43efacc9d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -514,6 +514,8 @@ fun debugState(context: Context) { Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size) Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size) + Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB") + LocalCache.notes.values.groupBy { it.event?.kind() }.forEach { Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ") } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index bc88e9ae0..408c56f35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -412,65 +412,97 @@ private fun ReactionDetailGallery( nav: (String) -> Unit, accountViewModel: AccountViewModel ) { - val zapsState by baseNote.live().zaps.observeAsState() - val boostsState by baseNote.live().boosts.observeAsState() - val reactionsState by baseNote.live().reactions.observeAsState() - val defaultBackgroundColor = MaterialTheme.colors.background val backgroundColor = remember { mutableStateOf(defaultBackgroundColor) } - val hasReactions by remember(zapsState, boostsState, reactionsState) { - derivedStateOf { - baseNote.zaps.isNotEmpty() || - baseNote.boosts.isNotEmpty() || - baseNote.reactions.isNotEmpty() - } - } + val hasReactions by baseNote.live().zaps.combineWith( + baseNote.live().boosts, + baseNote.live().reactions + ) { zapState, boostState, reactionState -> + zapState?.note?.zaps?.isNotEmpty() ?: false || + boostState?.note?.boosts?.isNotEmpty() ?: false || + reactionState?.note?.reactions?.isNotEmpty() ?: false + }.distinctUntilChanged().observeAsState( + baseNote.zaps.isNotEmpty() || baseNote.boosts.isNotEmpty() || baseNote.reactions.isNotEmpty() + ) if (hasReactions) { Row(verticalAlignment = CenterVertically, modifier = Modifier.padding(start = 10.dp, top = 5.dp)) { Column() { - val zapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.mapNotNull { it.value?.let { zapEvent -> CombinedZap(it.key, zapEvent) } }.toImmutableList() } } - val boostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.toImmutableList() } } - val likeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.toImmutableMap() } } - - val hasZapEvents by remember(zapsState) { derivedStateOf { baseNote.zaps.isNotEmpty() } } - val hasBoostEvents by remember(boostsState) { derivedStateOf { baseNote.boosts.isNotEmpty() } } - val hasLikeEvents by remember(reactionsState) { derivedStateOf { baseNote.reactions.isNotEmpty() } } - - if (hasZapEvents) { - RenderZapGallery( - zapEvents, - backgroundColor, - nav, - accountViewModel - ) - } - - if (hasBoostEvents) { - RenderBoostGallery( - boostEvents, - nav, - accountViewModel - ) - } - - if (hasLikeEvents) { - likeEvents.forEach { - val reactions = remember(it.value) { it.value.toImmutableList() } - RenderLikeGallery( - it.key, - reactions, - nav, - accountViewModel - ) - } - } + WatchZapAndRenderGallery(baseNote, backgroundColor, nav, accountViewModel) + WatchBoostsAndRenderGallery(baseNote, nav, accountViewModel) + WatchReactionsAndRenderGallery(baseNote, nav, accountViewModel) } } } } +@Composable +private fun WatchBoostsAndRenderGallery( + baseNote: Note, + nav: (String) -> Unit, + accountViewModel: AccountViewModel +) { + val boostsState by baseNote.live().boosts.observeAsState() + val boostsEvents by remember(boostsState) { + derivedStateOf { baseNote.boosts.toImmutableList() } + } + + if (boostsEvents.isNotEmpty()) { + RenderBoostGallery( + boostsEvents, + nav, + accountViewModel + ) + } +} + +@Composable +private fun WatchReactionsAndRenderGallery( + baseNote: Note, + nav: (String) -> Unit, + accountViewModel: AccountViewModel +) { + val reactionsState by baseNote.live().reactions.observeAsState() + val reactionEvents by remember(reactionsState) { + derivedStateOf { baseNote.reactions.toImmutableMap() } + } + + if (reactionEvents.isNotEmpty()) { + reactionEvents.forEach { + val reactions = remember(it.value) { it.value.toImmutableList() } + RenderLikeGallery( + it.key, + reactions, + nav, + accountViewModel + ) + } + } +} + +@Composable +private fun WatchZapAndRenderGallery( + baseNote: Note, + backgroundColor: MutableState, + nav: (String) -> Unit, + accountViewModel: AccountViewModel +) { + val zapsState by baseNote.live().zaps.observeAsState() + val zapEvents by remember(zapsState) { + derivedStateOf { baseNote.zaps.mapNotNull { it.value?.let { zapEvent -> CombinedZap(it.key, zapEvent) } }.toImmutableList() } + } + + if (zapEvents.isNotEmpty()) { + RenderZapGallery( + zapEvents, + backgroundColor, + nav, + accountViewModel + ) + } +} + @Composable private fun BoostWithDialog( baseNote: Note, diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 18527fdf8..828e20934 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -169,7 +169,11 @@ Signaler Discours haineux Signaler Nudité / Porno autres - + Marquer tous les Connus comme lu + Marquer tous les Nouveaux comme lu + Tout marquer comme lu + Clés de sauvegarde + ## Sauvegarde des clés et conseils de sécurité \n\nVotre compte est sécurisé par une clé secrète. La clé est une longue chaîne aléatoire commençant par **nsec1**. Toute personne ayant accès à votre clé secrète peut publier du contenu en utilisant votre identité. \n\n- Ne communiquez **jamais** votre clé secrète à un site Web ou un logiciel auquel vous ne faites pas confiance. @@ -177,5 +181,322 @@ \n- **Conservez** une sauvegarde sécurisée de votre clé secrète pour la récupération de compte. Nous vous recommandons d\'utiliser un gestionnaire de mots de passe. Clé secrète (nsec) copiée dans le presse-papiers - Clés de sauvegarde + Copier ma clé secrète + Échec de l'authentification + Erreur + "Créé par %1$s" + "Image de badge pour %1$s" + Vous avez reçu un nouveau Badge + Le Badge a été décerné à + Copie du contenu de la note dans le presse-papiers + Copie du @npub utilisateur dans le presse-papiers + Copie de l'ID de la note (@note1) dans le presse-papiers + Sélectionner le Texte + + "<Impossible de déchiffrer le message privé>\n\nVous avez été cité dans une conversation privée/cryptée entre %1$s et %2$s." + Ajouter un Nouveau Compte + Comptes + Sélectionner un Compte + Ajouter un Nouveau Compte + Compte actif + Avec une clé privée + Lecture seule, pas de clé privée + Retour + Sélectionner + Partager le Lien Navigateur + Partager + ID Author + ID Note + Copier le Texte + Supprimer + Ne plus suivre + Suivre + Demande de Suppression + Amethyst demandera que votre note soit supprimée des relais auxquels vous êtes actuellement connecté. Il n'y a aucune garantie que votre note sera définitivement supprimée de ces relais, ou d'autres relais où elle peut être stockée. + Bloquer + Supprimer + Bloquer + Signaler + Supprimer + Ne plus montrer + Spam ou arnaque + Grossièreté ou contenu haineux + Usurpation d'identité + Nudité ou contenu graphique + Comportement illégal + Bloquer un utilisateur cachera son contenu dans votre application. Vos notes sont toujours visibles publiquement, y compris pour les personnes que vous bloquez. Les utilisateurs bloqués sont listés sur l'écran Filtres de Sécurité. + + Signaler un Abus + Tous les rapports publiés seront visibles publiquement. + Fournir un contexte optionnel supplémentaire sur votre signalement... + Contexte additionnel + Raison + Sélectionner une raison... + Publier le signalement + Bloquer et Signaler + Bloquer + + + Favoris + Favoris Privés + Favoris Publics + Ajouter aux Favoris Privés + Ajouter aux Favoris Publics + Supprimer des Favoris Privés + Supprimer des Favoris Publics + + 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 + Wallet Connect Pubkey + Wallet Connect Relay + Wallet Connect Secret + Montrer la clé secrète + Clé privée nsec / hex + + Montant engagé en Sats + Message Sondage + Champs requis: + Bénéficiaires du Zap + Description du sondage primaire... + Option %s + Description de l'option du Sondage + Champs optionels: + Zap minimum + Zap maximum + Consensus + (0–100)% + Clôturer après + jours + Le Sondage est fermé aux nouveaux votes + Montant des Zap + Un seul vote par utilisateur est autorisé sur ce type de sondage + + "Recherche de l'Evénement %1$s" + + Ajouter un message public + Ajouter un message privé + Ajouter un message facture + + Merci pour tout votre travail ! + + Créer et Ajouter + Les auteurs du sondage ne peuvent pas voter dans leurs propres sondages. + + Ce contenu est le même depuis le message + Ce contenu a changé. L'auteur n'a peut-être pas vu ou approuvé le changement + + Ajouter une Image + Ajouter une Vidéo + Ajouter un Document + + Ajouter au Message + Description des contenus + Un bateau bleu sur une plage de sable blanc au coucher du soleil + + + Type de Zap + Zap Type pour toutes les options + + Public + Tout le monde peut voir la transaction et le message + + Privé + L'expéditeur et le receveur peuvent se voir et lire le message + + Anonyme + Le receveur et le public ne savent pas qui a envoyé le paiement + + Non-Zap + Aucune trace sur Nostr, seulement sur le Lightning + + + Serveur fichier + Adresse LN ou @Utilisateur + + imgur.com - de confiance + Imgur peut modifier le fichier + + nostrimg.com - de confiance + NostrImg peut modifier le fichier + + nostr.build - de confiance + Nostr.build peut modifier le fichier + + nostrfiles.dev - de confiance + Nostrfiles.dev peut modifier le fichier + + nostrcheck.me - de confiance + nostrcheck.me peut modifier le fichier + + + Verifiable Imgur (NIP-94) + Vérifie si Imgur a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir + + Verifiable NostrImg (NIP-94) + Vérifie si NostrImg a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir + + Verifiable Nostr.build (NIP-94) + Vérifie si Nostr.build a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir + + Verifiable Nostrfiles.dev (NIP-94) + Vérifie si Nostrfiles.dev a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir + + Verifiable Nostrcheck.me (NIP-94) + Vérifie si Nostrcheck.me a modifié le fichier. Nouveau NIP: certains clients pourraient ne pas le voir + + Vos relais (NIP-95) + Les fichiers sont hébergés par vos relais. Nouveau NIP: vérifiez s'ils sont supportés + + Configuration Tor/Orbot + Se connecter à travers la configuration Orbot + + Se déconnecter de Orbot/Tor? + Vos données seront immédiatement transférées sur le réseau classique + Oui + Non + + + Liste des Abonnés + Tous les abonnements + Global + + ## Se connecter à travers Tor avec Orbot + \n\n1. Installe [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) + \n2. Démarre Orbot + \n3. Dans Orbot, vérifie le port Socks. Par défaut 9050 + \n4. Si nécessaire change le port dans Orbot + \n5. Configure le port Socks sur cet écran + \n6. Appuyer sur le bouton Activer pour utiliser Orbot comme proxy + + Port Socks Orbot + Numéro de port invalide + Utiliser Orbot + Déconnecter Tor/Orbot + + Messages privés + Vous notifie lorsqu'un message privé est reçu + + Zaps reçus + Vous notifie lorsque quelqu'un vous zap + %1$s sats + De %1$s + pour %1$s + + + Notifier: + + Rejoindre Conversation + Utilisateur ou Groupe ID + npub, nevent ou hex + Créer + Rejoindre + + Aujourd'hui + + Avertissement de contenu + Ce message contient du contenu sensible que certaines personnes peuvent trouver offensant ou dérangeant + Toujours cacher les contenus sensibles + Toujours afficher les contenus sensibles + Toujours afficher les avertissements de contenu + + Recommendations: + Filtrer le spam des étrangers + Avertir lorsque les messages ont été rapportés par vos abonnés + + Nouveau symbole de réaction + Aucun type de réaction sélectionné. Appuyer longuement pour changer + + Zapraiser + Ajoute une quantité cible de sats pour ce message. Les clients compatible peuvent afficher une barre de progrès pour encourager les dons + Quantité ciblée en Sats + + Zapraiser à %1$s. %2$s sats pour objectif + Lire depuis le Relay + Écrire sur le Relay + Une erreur s'est produite en récupérant les informations du relay depuis %1$s + Propriétaire + Version + Logiciel + Contact + NIPs supportés + Frais d'admission + URL de payments + Limitations + Pays + Langages + Tags + Politique de publication + Longueur du message + Abonnements + Filtres + Longueur de l'id de souscription + Préfixe minimum + Tags d'événement maximum + Longueur du contenu + PoW minimum + Authentification + Paiement + Jeton Cashu + Échanger + Adresse Lightning non définie + Jeton copié dans le presse-papiers + + EN DIRECT + DÉCONNECTÉ + TERMINÉ + PLANIFIÉ + + Livestream est déconnecté + Livestream terminé + Se déconnecter supprime toutes vos informations locales. Assurez-vous d'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ? + Tags suivis + + Relays + + Direct + Communauté + Chats + Messages approuvés + + Ce groupe n'a pas de description ou de règles. Parlez au propriétaire pour en ajouter une + Cette communauté n'a pas de description. Parlez au propriétaire pour en ajouter une + + Contenu sensible + Ajouter un avertissement de contenu sensible avant de montrer ce contenu + Paramètres + Toujours + Wifi uniquement + Jamais + + Système + Clair + Sombre + Préférences de l'application + Langage + Thème + Chargement automatique des images/gifs + Démarrage automatique des vidéos + Prévisualisation automatique des liens + Charger l'image + + Spammeurs + + Silencieux. Cliquer pour réactiver le son + Son activé. Cliquer pour mettre en silencieux + Recherche d'enregistrements locaux et distants + + L'adresse Nostr a été vérfiée + La vérification de l'adresse Nostr a échouée + Vérification de l'adresse Nostr + Tout sélectionner/désélectionner + Défaut + Sélectionner un relay pour continuer + + Transférer les Zaps à: + Les clients compatibles transmettront des zaps sur l'adresse LN ou le profil Utilisateur ci-dessous au lieu du vôtre. + + Révéler la localisation comme + Ajoute un Geohash de votre emplacement au message. Le public saura que vous êtes à moins de 5km de l'emplacement actuel + + Ajoute un avertissement de contenu sensible avant de montrer votre contenu. C'est idéal pour tout contenu NSFW ou contenu que certaines personnes peuvent trouver offensant ou dérangeant