Merge remote-tracking branch 'origin/main' into claude/cashu-wallet-wizard-0zr280

This commit is contained in:
Claude
2026-06-27 16:55:49 +00:00
31 changed files with 1261 additions and 7 deletions
+10
View File
@@ -348,6 +348,16 @@ composeCompiler {
dependencies {
implementation(platform(libs.androidx.compose.bom))
// Compose composition tracing — DEBUG ONLY, profiling aid (not shipped). Makes each
// recomposition show up as a NAMED slice in Perfetto system traces so we can see which
// composable recomposes (e.g. during the cold-start feed first-paint). All Apache-2.0.
// Usage: runtime-enable, then capture a Perfetto trace with the `track_event` data source:
// adb shell am broadcast -a androidx.tracing.perfetto.action.ENABLE_TRACING \
// -n com.vitorpamplona.amethyst.debug/androidx.tracing.perfetto.TracingReceiver
debugImplementation("androidx.compose.runtime:runtime-tracing")
debugImplementation("androidx.tracing:tracing-perfetto:1.0.0")
debugImplementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
implementation(project(":quartz"))
implementation(project(":commons"))
implementation(project(":nestsClient"))
@@ -108,6 +108,7 @@ private object PrefKeys {
const val DEFAULT_NAPPLETS_FOLLOW_LIST = "defaultNappletsFollowList"
const val DEFAULT_NSITES_FOLLOW_LIST = "defaultNsitesFollowList"
const val DEFAULT_WORKOUTS_FOLLOW_LIST = "defaultWorkoutsFollowList"
const val DEFAULT_GIT_REPOSITORIES_FOLLOW_LIST = "defaultGitRepositoriesFollowList"
const val DEFAULT_CALENDARS_FOLLOW_LIST = "defaultCalendarsFollowList"
const val DEFAULT_PRODUCTS_FOLLOW_LIST = "defaultProductsFollowList"
const val DEFAULT_SHORTS_FOLLOW_LIST = "defaultShortsFollowList"
@@ -426,6 +427,7 @@ object LocalPreferences {
putString(PrefKeys.DEFAULT_NAPPLETS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultNappletsFollowList.value))
putString(PrefKeys.DEFAULT_NSITES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultNsitesFollowList.value))
putString(PrefKeys.DEFAULT_WORKOUTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultWorkoutsFollowList.value))
putString(PrefKeys.DEFAULT_GIT_REPOSITORIES_FOLLOW_LIST, JsonMapper.toJson(settings.defaultGitRepositoriesFollowList.value))
putString(PrefKeys.DEFAULT_CALENDARS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultCalendarsFollowList.value))
putString(PrefKeys.DEFAULT_PRODUCTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultProductsFollowList.value))
putString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, JsonMapper.toJson(settings.defaultShortsFollowList.value))
@@ -790,6 +792,7 @@ object LocalPreferences {
defaultNappletsFollowList = MutableStateFlow(followListPrefs.napplets),
defaultNsitesFollowList = MutableStateFlow(followListPrefs.nsites),
defaultWorkoutsFollowList = MutableStateFlow(followListPrefs.workouts),
defaultGitRepositoriesFollowList = MutableStateFlow(followListPrefs.gitRepositories),
defaultCalendarsFollowList = MutableStateFlow(followListPrefs.calendars),
defaultProductsFollowList = MutableStateFlow(followListPrefs.products),
defaultShortsFollowList = MutableStateFlow(followListPrefs.shorts),
@@ -882,6 +885,7 @@ object LocalPreferences {
val napplets: TopFilter,
val nsites: TopFilter,
val workouts: TopFilter,
val gitRepositories: TopFilter,
val calendars: TopFilter,
val products: TopFilter,
val shorts: TopFilter,
@@ -937,6 +941,7 @@ object LocalPreferences {
napplets = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_NAPPLETS_FOLLOW_LIST, null), TopFilter.Global),
nsites = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_NSITES_FOLLOW_LIST, null), TopFilter.Global),
workouts = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_WORKOUTS_FOLLOW_LIST, null), TopFilter.Global),
gitRepositories = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_GIT_REPOSITORIES_FOLLOW_LIST, null), TopFilter.Global),
calendars = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_CALENDARS_FOLLOW_LIST, null), TopFilter.Global),
products = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_PRODUCTS_FOLLOW_LIST, null), TopFilter.AroundMe),
shorts = parseTopFilterOrDefault(getString(PrefKeys.DEFAULT_SHORTS_FOLLOW_LIST, null), TopFilter.Global),
@@ -543,6 +543,9 @@ class Account(
val liveWorkoutsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultWorkoutsFollowList)
val liveWorkoutsFollowListsPerRelay = OutboxLoaderState(liveWorkoutsFollowLists, cache, scope).flow
val liveGitRepositoriesFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultGitRepositoriesFollowList)
val liveGitRepositoriesFollowListsPerRelay = OutboxLoaderState(liveGitRepositoriesFollowLists, cache, scope).flow
val liveCalendarsFollowLists: StateFlow<IFeedTopNavFilter> = topNavFilterFlow(settings.defaultCalendarsFollowList)
val liveCalendarsFollowListsPerRelay = OutboxLoaderState(liveCalendarsFollowLists, cache, scope).flow
@@ -192,6 +192,7 @@ class AccountSettings(
val defaultNappletsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultNsitesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultWorkoutsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultGitRepositoriesFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultCalendarsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
val defaultProductsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.AroundMe),
val defaultShortsFollowList: MutableStateFlow<TopFilter> = MutableStateFlow(TopFilter.Global),
@@ -675,6 +676,17 @@ class AccountSettings(
}
}
fun changeDefaultGitRepositoriesFollowList(name: FeedDefinition) {
changeDefaultGitRepositoriesFollowList(name.code)
}
fun changeDefaultGitRepositoriesFollowList(name: TopFilter) {
if (defaultGitRepositoriesFollowList.value != name) {
defaultGitRepositoriesFollowList.tryEmit(name)
saveAccountSettings()
}
}
fun changeDefaultCalendarsFollowList(name: FeedDefinition) {
changeDefaultCalendarsFollowList(name.code)
}
@@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.datasource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.datasource.GeoHashFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.datasource.RepositoryFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.GitRepositoriesFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.datasource.HashtagFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssembler
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssembler
@@ -127,6 +128,7 @@ class RelaySubscriptionsCoordinator(
val polls = PollsFilterAssembler(client)
val pictures = PicturesFilterAssembler(client)
val workouts = WorkoutsFilterAssembler(client)
val gitRepositories = GitRepositoriesFilterAssembler(client)
val calendars = CalendarsFilterAssembler(client)
val products = ProductsFilterAssembler(client)
val shorts = ShortsFilterAssembler(client)
@@ -177,6 +179,7 @@ class RelaySubscriptionsCoordinator(
polls,
pictures,
workouts,
gitRepositories,
calendars,
products,
shorts,
@@ -66,6 +66,7 @@ object ScrollStateKeys {
const val COMMUNITIES_LIST = "CommunitiesListFeed"
const val PICTURES_SCREEN = "PicturesFeed"
const val WORKOUTS_SCREEN = "WorkoutsFeed"
const val GIT_REPOSITORIES_SCREEN = "GitRepositoriesFeed"
const val CALENDARS_SCREEN = "CalendarsFeed"
const val CALENDAR_COLLECTIONS_SCREEN = "CalendarCollectionsFeed"
const val PRODUCTS_SCREEN = "ProductsFeed"
@@ -134,6 +134,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.FollowPack
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashPostScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geohash.GeoHashScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.GitRepositoriesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagPostScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
@@ -323,6 +324,7 @@ fun BuildNavigation(
composableFromBottomArgs<Route.AwardBadge> { AwardBadgeScreen(it.kind, it.pubKeyHex, it.dTag, accountViewModel, nav) }
composableFromEnd<Route.Pictures> { PicturesScreen(accountViewModel, nav) }
composableFromEnd<Route.Workouts> { WorkoutsScreen(accountViewModel, nav) }
composableFromEnd<Route.GitRepositories> { GitRepositoriesScreen(accountViewModel, nav) }
composableFromEnd<Route.SoftwareApps> { SoftwareAppsScreen(accountViewModel, nav) }
composableFromEnd<Route.Napplets> { NappletsScreen(accountViewModel, nav) }
composableFromEnd<Route.Nsites> { NsitesScreen(accountViewModel, nav) }
@@ -52,6 +52,7 @@ enum class NavBarItem {
ARTICLES,
PICTURES,
WORKOUTS,
GIT_REPOSITORIES,
SOFTWARE_APPS,
NAPPLETS,
NSITES,
@@ -219,6 +220,13 @@ val NavBarCatalog: Map<NavBarItem, NavBarItemDef> =
icon = MaterialSymbols.DirectionsRun,
resolveRoute = { Route.Workouts },
),
NavBarItem.GIT_REPOSITORIES to
NavBarItemDef(
id = NavBarItem.GIT_REPOSITORIES,
labelRes = R.string.git_repositories,
icon = MaterialSymbols.Code,
resolveRoute = { Route.GitRepositories },
),
NavBarItem.SOFTWARE_APPS to
NavBarItemDef(
id = NavBarItem.SOFTWARE_APPS,
@@ -426,6 +434,7 @@ val DrawerFeedsItems: List<NavBarItem> =
NavBarItem.POLLS,
NavBarItem.PRODUCTS,
NavBarItem.WORKOUTS,
NavBarItem.GIT_REPOSITORIES,
NavBarItem.LIVE_STREAMS,
NavBarItem.NESTS,
NavBarItem.COMMUNITIES,
@@ -85,6 +85,8 @@ sealed class Route {
@Serializable object Workouts : Route()
@Serializable object GitRepositories : Route()
@Serializable object SoftwareApps : Route()
@Serializable object Napplets : Route()
@@ -41,7 +41,9 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.MemorySnapshot
import com.vitorpamplona.amethyst.collectMemorySnapshot
import com.vitorpamplona.amethyst.isDebug
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
@Composable
fun MemoryUsageChip() {
@@ -52,7 +54,12 @@ fun MemoryUsageChip() {
val snapshot by produceState<MemorySnapshot?>(null) {
while (true) {
value = collectMemorySnapshot(context)
// collectMemorySnapshot reads coil3.disk.DiskLruCache.size(), which is @Synchronized and
// contends with the disk cache's own journal I/O. On cold start that lock is held by a
// background worker for seconds (initial journal read + the burst of image writes), so
// running this on the produceState default (main) dispatcher froze the UI thread —
// the "Loading account" frame couldn't repaint until size() returned. Collect off-main.
value = withContext(Dispatchers.IO) { collectMemorySnapshot(context) }
delay(2_000)
}
}
@@ -25,6 +25,7 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -730,6 +731,8 @@ private fun RenderGitRepositoryEvent(
val summary = noteEvent.description()
val web = noteEvent.web()
val clone = noteEvent.clone()
val topics = remember(noteEvent) { noteEvent.hashtags().filter { it.isNotBlank() } }
val isPersonalFork = remember(noteEvent) { noteEvent.isPersonalFork() }
GitCardContainer {
Row(
@@ -759,6 +762,15 @@ private fun RenderGitRepositoryEvent(
overflow = TextOverflow.Ellipsis,
)
}
if (isPersonalFork) {
TypeChip(
text = stringRes(id = R.string.git_repo_personal_fork),
background = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
symbol = MaterialSymbols.AltRoute,
)
}
}
summary?.let {
@@ -791,5 +803,21 @@ private fun RenderGitRepositoryEvent(
}
}
}
if (topics.isNotEmpty()) {
Spacer(modifier = HalfDoubleVertSpacer)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(Size5dp),
verticalArrangement = Arrangement.spacedBy(Size5dp),
) {
topics.forEach { topic ->
TypeChip(
text = "#$topic",
background = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f),
contentColor = MaterialTheme.colorScheme.grayText,
)
}
}
}
}
}
@@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.nip99Classifieds.D
import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal.DraftEventsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.dal.BrowseEmojiSetsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.dal.FollowPacksFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.dal.GitRepositoriesFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeEverythingFeedFilter
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter
@@ -111,6 +112,7 @@ class AccountFeedContentStates(
val picturesFeed = FeedContentState(PictureFeedFilter(account), scope, LocalCache)
val workoutsFeed = FeedContentState(WorkoutFeedFilter(account), scope, LocalCache)
val gitRepositoriesFeed = FeedContentState(GitRepositoriesFeedFilter(account), scope, LocalCache)
val calendarAppointmentsFeed = FeedContentState(CalendarAppointmentsFeedFilter(account), scope, LocalCache)
val calendarCollectionsFeed = FeedContentState(CalendarCollectionsFeedFilter(account), scope, LocalCache)
val productsFeed = FeedContentState(ProductsFeedFilter(account), scope, LocalCache)
@@ -228,6 +230,7 @@ class AccountFeedContentStates(
picturesFeed.updateFeedWith(newNotes)
workoutsFeed.updateFeedWith(newNotes)
gitRepositoriesFeed.updateFeedWith(newNotes)
productsFeed.updateFeedWith(newNotes)
shortsFeed.updateFeedWith(newNotes)
publicChatsFeed.updateFeedWith(newNotes)
@@ -289,6 +292,7 @@ class AccountFeedContentStates(
picturesFeed.deleteFromFeed(newNotes)
workoutsFeed.deleteFromFeed(newNotes)
gitRepositoriesFeed.deleteFromFeed(newNotes)
productsFeed.deleteFromFeed(newNotes)
shortsFeed.deleteFromFeed(newNotes)
publicChatsFeed.deleteFromFeed(newNotes)
@@ -346,6 +350,7 @@ class AccountFeedContentStates(
picturesFeed.trimToSize(maxItems)
workoutsFeed.trimToSize(maxItems)
gitRepositoriesFeed.trimToSize(maxItems)
calendarAppointmentsFeed.trimToSize(maxItems)
calendarCollectionsFeed.trimToSize(maxItems)
productsFeed.trimToSize(maxItems)
@@ -34,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.list.datasource
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.datasource.BrowseEmojiSetsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.datasource.FollowPacksFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.GitRepositoriesFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.datasource.HomeFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.datasource.LiveStreamsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.longs.datasource.LongsFilterAssemblerSubscription
@@ -96,6 +97,8 @@ private fun PreloadFor(
NavBarItem.WORKOUTS -> WorkoutsFilterAssemblerSubscription(accountViewModel)
NavBarItem.GIT_REPOSITORIES -> GitRepositoriesFilterAssemblerSubscription(accountViewModel)
NavBarItem.SOFTWARE_APPS -> SoftwareAppsFilterAssemblerSubscription(accountViewModel)
// Napplets & nSites read directly from the local cache; their screens open the discovery
@@ -0,0 +1,105 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.feeds.RenderFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.SaveableFeedContentState
import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.GitRepositoriesFilterAssemblerSubscription
@Composable
fun GitRepositoriesScreen(
accountViewModel: AccountViewModel,
nav: INav,
) {
GitRepositoriesScreen(
gitRepositoriesFeedContentState = accountViewModel.feedStates.gitRepositoriesFeed,
accountViewModel = accountViewModel,
nav = nav,
)
}
@Composable
fun GitRepositoriesScreen(
gitRepositoriesFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
nav: INav,
) {
WatchLifecycleAndUpdateModel(gitRepositoriesFeedContentState)
WatchAccountForGitRepositoriesScreen(gitRepositoriesFeedContentState = gitRepositoriesFeedContentState, accountViewModel = accountViewModel)
GitRepositoriesFilterAssemblerSubscription(accountViewModel)
DisappearingScaffold(
isInvertedLayout = false,
topBar = {
GitRepositoriesTopBar(accountViewModel, nav)
},
bottomBar = {
AppBottomBar(Route.GitRepositories, nav, accountViewModel) { route ->
if (route == Route.GitRepositories) {
gitRepositoriesFeedContentState.sendToTop()
} else {
nav.navBottomBar(route)
}
}
},
accountViewModel = accountViewModel,
) {
RefresheableBox(gitRepositoriesFeedContentState, true) {
SaveableFeedContentState(gitRepositoriesFeedContentState, scrollStateKey = ScrollStateKeys.GIT_REPOSITORIES_SCREEN) { listState ->
RenderFeedContentState(
feedContentState = gitRepositoriesFeedContentState,
accountViewModel = accountViewModel,
listState = listState,
nav = nav,
routeForLastRead = "GitRepositoriesFeed",
)
}
}
}
}
@Composable
fun WatchAccountForGitRepositoriesScreen(
gitRepositoriesFeedContentState: FeedContentState,
accountViewModel: AccountViewModel,
) {
val listState by accountViewModel.account.liveGitRepositoriesFollowLists.collectAsStateWithLifecycle()
val hiddenUsers =
accountViewModel.account.hiddenUsers.flow
.collectAsStateWithLifecycle()
LaunchedEffect(accountViewModel, listState, hiddenUsers) {
gitRepositoriesFeedContentState.checkKeysInvalidateDataAndSendToTop()
}
}
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.FeedFilterSpinner
import com.vitorpamplona.amethyst.ui.navigation.topbars.UserDrawerSearchTopBar
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
import com.vitorpamplona.amethyst.ui.screen.TopNavFilterState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
@Composable
fun GitRepositoriesTopBar(
accountViewModel: AccountViewModel,
nav: INav,
) {
UserDrawerSearchTopBar(accountViewModel, nav) {
val list by accountViewModel.account.settings.defaultGitRepositoriesFollowList
.collectAsStateWithLifecycle()
GitRepositoriesTopNavFilterBar(
followListsModel = accountViewModel.feedStates.feedListOptions,
listName = list,
accountViewModel = accountViewModel,
onChange = accountViewModel.account.settings::changeDefaultGitRepositoriesFollowList,
)
}
}
@Composable
private fun GitRepositoriesTopNavFilterBar(
followListsModel: TopNavFilterState,
listName: TopFilter,
accountViewModel: AccountViewModel,
onChange: (FeedDefinition) -> Unit,
) {
val allLists by followListsModel.kind3GlobalPeopleRoutes.collectAsStateWithLifecycle()
FeedFilterSpinner(
placeholderCode = listName,
explainer = stringRes(R.string.select_list_to_filter),
options = allLists,
onSelect = onChange,
accountViewModel = accountViewModel,
)
}
@@ -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.screen.loggedIn.gitRepositories.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.filterIntoSet
import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FilterByListParams
import com.vitorpamplona.amethyst.ui.dal.sortedByDefaultFeedOrder
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
class GitRepositoriesFeedFilter(
val account: Account,
) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + followList().code
override fun limit() = 200
fun followList(): TopFilter = account.settings.defaultGitRepositoriesFollowList.value
fun TopFilter.isMuteList() = this is TopFilter.MuteList
fun TopFilter.isBlockList() = this is TopFilter.PeopleList && this.address == account.blockPeopleList.getBlockListAddress()
fun TopFilter.wantsToSeeNegativeStuff() = isMuteList() || isBlockList()
override fun showHiddenKey(): Boolean = followList().wantsToSeeNegativeStuff()
override fun feed(): List<Note> {
val params = buildFilterParams(account)
val notes =
LocalCache.addressables.filterIntoSet(GitRepositoryEvent.KIND) { _, it ->
val noteEvent = it.event
noteEvent is GitRepositoryEvent && params.match(noteEvent, it.relays)
}
return sort(notes)
}
override fun applyFilter(newItems: Set<Note>): Set<Note> = innerApplyFilter(newItems)
fun buildFilterParams(account: Account): FilterByListParams =
FilterByListParams.create(
account.liveGitRepositoriesFollowLists.value,
account.hiddenUsers.flow.value,
)
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val params = buildFilterParams(account)
return collection.filterTo(HashSet()) {
val noteEvent = it.event
noteEvent is GitRepositoryEvent && params.match(noteEvent, it.relays)
}
}
override fun sort(items: Set<Note>): List<Note> = items.sortedByDefaultFeedOrder()
}
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource
import com.vitorpamplona.amethyst.model.topNavFeeds.IFeedTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByAllCommunities
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByAuthors
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByCommunity
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByFollows
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByGeohashes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByHashtag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesByMutedAuthors
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies.filterGitRepositoriesGlobal
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
fun makeGitRepositoriesFilter(
feedSettings: IFeedTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> =
when (feedSettings) {
is AllCommunitiesTopNavPerRelayFilterSet -> filterGitRepositoriesByAllCommunities(feedSettings, since, defaultSince)
is AllFollowsTopNavPerRelayFilterSet -> filterGitRepositoriesByFollows(feedSettings, since, defaultSince)
is AuthorsTopNavPerRelayFilterSet -> filterGitRepositoriesByAuthors(feedSettings, since, defaultSince)
is GlobalTopNavPerRelayFilterSet -> filterGitRepositoriesGlobal(feedSettings, since, defaultSince)
is HashtagTopNavPerRelayFilterSet -> filterGitRepositoriesByHashtag(feedSettings, since, defaultSince)
is LocationTopNavPerRelayFilterSet -> filterGitRepositoriesByGeohashes(feedSettings, since, defaultSince)
is MutedAuthorsTopNavPerRelayFilterSet -> filterGitRepositoriesByMutedAuthors(feedSettings, since, defaultSince)
is SingleCommunityTopNavPerRelayFilterSet -> filterGitRepositoriesByCommunity(feedSettings, since, defaultSince)
else -> emptyList()
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.commons.relayClient.composeSubscriptionManagers.ComposeSubscriptionManager
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountFeedContentStates
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import kotlinx.coroutines.CoroutineScope
class GitRepositoriesQueryState(
val account: Account,
val feedStates: AccountFeedContentStates,
val scope: CoroutineScope,
)
@Stable
class GitRepositoriesFilterAssembler(
client: INostrClient,
) : ComposeSubscriptionManager<GitRepositoriesQueryState>() {
val group =
listOf(
GitRepositoriesSubAssembler(client, ::allKeys),
)
override fun invalidateKeys() = invalidateFilters()
override fun invalidateFilters() = group.forEach { it.invalidateFilters() }
override fun destroy() = group.forEach { it.destroy() }
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.commons.relayClient.subscriptions.LifecycleAwareKeyDataSourceSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun GitRepositoriesFilterAssemblerSubscription(accountViewModel: AccountViewModel) {
GitRepositoriesFilterAssemblerSubscription(
accountViewModel.dataSources().gitRepositories,
accountViewModel,
)
}
@Composable
fun GitRepositoriesFilterAssemblerSubscription(
dataSource: GitRepositoriesFilterAssembler,
accountViewModel: AccountViewModel,
) {
val state =
remember(accountViewModel.account) {
GitRepositoriesQueryState(accountViewModel.account, accountViewModel.feedStates, accountViewModel.viewModelScope)
}
LifecycleAwareKeyDataSourceSubscription(state, dataSource)
}
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource
import com.vitorpamplona.amethyst.model.TopFilter
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
class GitRepositoriesSubAssembler(
client: INostrClient,
allKeys: () -> Set<GitRepositoriesQueryState>,
) : PerUserAndFollowListEoseManager<GitRepositoriesQueryState, TopFilter>(client, allKeys) {
override fun updateFilter(
key: GitRepositoriesQueryState,
since: SincePerRelayMap?,
): List<RelayBasedFilter> {
val feedSettings = key.followsPerRelay()
return makeGitRepositoriesFilter(feedSettings, since, key.feedStates.gitRepositoriesFeed.lastNoteCreatedAtIfFilled())
}
override fun user(key: GitRepositoriesQueryState) = key.account.userProfile()
override fun list(key: GitRepositoriesQueryState) = key.listName()
fun GitRepositoriesQueryState.listNameFlow() = account.settings.defaultGitRepositoriesFollowList
fun GitRepositoriesQueryState.listName() = listNameFlow().value
fun GitRepositoriesQueryState.followsPerRelayFlow() = account.liveGitRepositoriesFollowListsPerRelay
fun GitRepositoriesQueryState.followsPerRelay() = followsPerRelayFlow().value
val userJobMap = mutableMapOf<User, List<Job>>()
@OptIn(FlowPreview::class)
override fun newSub(key: GitRepositoriesQueryState): Subscription {
val user = user(key)
userJobMap[user]?.forEach { it.cancel() }
userJobMap[user] =
listOf(
key.scope.launch(Dispatchers.IO) {
key.listNameFlow().collectLatest {
invalidateFilters()
}
},
key.scope.launch(Dispatchers.IO) {
key.followsPerRelayFlow().sample(500).collectLatest {
invalidateFilters()
}
},
key.account.scope.launch(Dispatchers.IO) {
key.feedStates.gitRepositoriesFeed.lastNoteCreatedAtWhenFullyLoaded.sample(5000).collectLatest {
invalidateFilters()
}
},
)
return super.newSub(key)
}
override fun endSub(
key: User,
subId: String,
) {
super.endSub(key, subId)
userJobMap[key]?.forEach { it.cancel() }
}
}
@@ -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.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.allcommunities.AllCommunitiesTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.community.SingleCommunityTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
val GitRepositoriesFromCommunityKinds =
listOf(
GitRepositoryEvent.KIND,
)
val GitRepositoriesFromCommunityKindsStr =
listOf(
GitRepositoryEvent.KIND.toString(),
)
fun filterGitRepositoriesFromAllCommunities(
relay: NormalizedRelayUrl,
communities: Set<String>,
since: Long? = null,
): List<RelayBasedFilter> {
val communityList = communities.sorted()
return listOf(
// approved
RelayBasedFilter(
relay = relay,
filter =
Filter(
kinds = CommunityPostApprovalEvent.KIND_LIST,
tags =
mapOf(
"a" to communityList,
"k" to GitRepositoriesFromCommunityKindsStr,
),
limit = communityList.size * 20,
since = since,
),
),
// not approved
RelayBasedFilter(
relay = relay,
filter =
Filter(
tags = mapOf("a" to communityList),
kinds = GitRepositoriesFromCommunityKinds,
limit = communityList.size * 20,
since = since,
),
),
)
}
fun filterGitRepositoriesByAllCommunities(
communitySet: AllCommunitiesTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> {
if (communitySet.set.isEmpty()) return emptyList()
return communitySet.set
.mapNotNull {
filterGitRepositoriesFromAllCommunities(
relay = it.key,
communities = it.value.communities,
since = since?.get(it.key)?.time ?: defaultSince,
)
}.flatten()
}
fun filterGitRepositoriesFromCommunity(
relay: NormalizedRelayUrl,
community: String,
authors: Set<String>?,
since: Long? = null,
): List<RelayBasedFilter> {
val authors = authors?.sorted()
return listOf(
// approved
RelayBasedFilter(
relay = relay,
filter =
Filter(
authors = authors,
kinds = CommunityPostApprovalEvent.KIND_LIST,
tags =
mapOf(
"a" to listOf(community),
"k" to GitRepositoriesFromCommunityKindsStr,
),
limit = 100,
since = since,
),
),
// not approved
RelayBasedFilter(
relay = relay,
filter =
Filter(
authors = authors,
tags = mapOf("a" to listOf(community)),
kinds = GitRepositoriesFromCommunityKinds,
limit = 100,
since = since,
),
),
)
}
fun filterGitRepositoriesByCommunity(
communitySet: SingleCommunityTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> {
if (communitySet.set.isEmpty()) return emptyList()
return communitySet.set
.mapNotNull {
filterGitRepositoriesFromCommunity(
relay = it.key,
community = it.value.community,
authors = it.value.authors,
since = since?.get(it.key)?.time ?: defaultSince,
)
}.flatten()
}
@@ -0,0 +1,92 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.author.AuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
fun filterGitRepositoriesByAuthors(
relay: NormalizedRelayUrl,
authors: Set<HexKey>,
since: Long? = null,
): List<RelayBasedFilter> {
val authorList = authors.sorted()
return listOf(
RelayBasedFilter(
relay = relay,
filter =
Filter(
authors = authorList,
kinds = listOf(GitRepositoryEvent.KIND),
limit = 200,
since = since,
),
),
)
}
fun filterGitRepositoriesByAuthors(
authorSet: AuthorsTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> {
if (authorSet.set.isEmpty()) return emptyList()
return authorSet.set
.mapNotNull {
if (it.value.authors.isEmpty()) {
null
} else {
filterGitRepositoriesByAuthors(
relay = it.key,
authors = it.value.authors,
since = since?.get(it.key)?.time ?: defaultSince,
)
}
}.flatten()
}
fun filterGitRepositoriesByMutedAuthors(
authorSet: MutedAuthorsTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> {
if (authorSet.set.isEmpty()) return emptyList()
return authorSet.set
.mapNotNull {
if (it.value.authors.isEmpty()) {
null
} else {
filterGitRepositoriesByAuthors(
relay = it.key,
authors = it.value.authors,
since = since?.get(it.key)?.time ?: defaultSince,
)
}
}.flatten()
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies
import com.vitorpamplona.amethyst.model.topNavFeeds.allFollows.AllFollowsTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
fun filterGitRepositoriesByFollows(
followsSet: AllFollowsTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> {
if (followsSet.set.isEmpty()) return emptyList()
return followsSet.set.flatMap {
val since = since?.get(it.key)?.time ?: defaultSince
val relay = it.key
listOfNotNull(
it.value.authors?.let {
filterGitRepositoriesByAuthors(relay, it, since)
},
).flatten()
}
}
@@ -0,0 +1,70 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies
import com.vitorpamplona.amethyst.model.topNavFeeds.aroundMe.LocationTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
fun filterGitRepositoriesByGeohashes(
relay: NormalizedRelayUrl,
geotags: Set<String>,
since: Long?,
): List<RelayBasedFilter> {
if (geotags.isEmpty()) return emptyList()
return listOf(
RelayBasedFilter(
relay = relay,
filter =
Filter(
kinds = listOf(GitRepositoryEvent.KIND),
tags = mapOf("g" to geotags.sorted()),
limit = 100,
since = since,
),
),
)
}
fun filterGitRepositoriesByGeohashes(
geoSet: LocationTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long?,
): List<RelayBasedFilter> {
if (geoSet.set.isEmpty()) return emptyList()
return geoSet.set
.mapNotNull {
if (it.value.geotags.isEmpty()) {
null
} else {
filterGitRepositoriesByGeohashes(
relay = it.key,
geotags = it.value.geotags,
since = since?.get(it.key)?.time ?: defaultSince,
)
}
}.flatten()
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies
import com.vitorpamplona.amethyst.model.topNavFeeds.hashtag.HashtagTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
fun filterGitRepositoriesByHashtag(
relay: NormalizedRelayUrl,
hashtags: Set<String>,
since: Long? = null,
): List<RelayBasedFilter> =
listOf(
RelayBasedFilter(
relay = relay,
filter =
Filter(
kinds = listOf(GitRepositoryEvent.KIND),
tags = mapOf("t" to hashtags.toList()),
limit = 200,
since = since,
),
),
)
fun filterGitRepositoriesByHashtag(
hashtagSet: HashtagTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> {
if (hashtagSet.set.isEmpty()) return emptyList()
return hashtagSet.set
.mapNotNull { relayHashSet ->
if (relayHashSet.value.hashtags.isEmpty()) {
null
} else {
filterGitRepositoriesByHashtag(
relay = relayHashSet.key,
hashtags = relayHashSet.value.hashtags,
since = since?.get(relayHashSet.key)?.time ?: defaultSince,
)
}
}.flatten()
}
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepositories.datasource.subassemblies
import com.vitorpamplona.amethyst.model.topNavFeeds.global.GlobalTopNavPerRelayFilterSet
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.utils.TimeUtils
fun filterGitRepositoriesGlobal(
relays: GlobalTopNavPerRelayFilterSet,
since: SincePerRelayMap?,
defaultSince: Long? = null,
): List<RelayBasedFilter> {
if (relays.set.isEmpty()) return emptyList()
return relays.set.map {
val since = since?.get(it.key)?.time ?: defaultSince ?: TimeUtils.oneWeekAgo()
RelayBasedFilter(
relay = it.key,
filter =
Filter(
kinds = listOf(GitRepositoryEvent.KIND),
limit = 200,
since = since,
),
)
}
}
@@ -719,6 +719,7 @@
<item quantity="other">Este nApplet quiere pagar una factura Lightning de %1$d sats.</item>
</plurals>
<string name="nip82_repository_label">Fuente: %1$s</string>
<string name="nip82_version_label">v%1$s</string>
<string name="nip82_download">Descargar</string>
<string name="nip82_section_about">Acerca de</string>
<string name="nip82_section_platforms">Plataformas</string>
@@ -878,6 +879,7 @@
<string name="route_music_tracks">Música</string>
<string name="route_music_playlists">Listas de reproducción</string>
<string name="route_podcast_episodes">Episodios</string>
<string name="route_podcasts">Podcasts</string>
<string name="podcast_view_episodes">Ver episodios</string>
<string name="podcast_no_episodes">Aún no se han encontrado episodios</string>
<plurals name="podcast_episode_count">
@@ -915,6 +917,7 @@
<string name="add_hashtag_label">Añadir hashtag</string>
<string name="add_hashtag_label_title">Añadir un hashtag</string>
<string name="add_hashtag_label_explainer">Etiqueta públicamente esta publicación con un hashtag (etiqueta NIP-32). Las personas que te siguen lo verán en el feed de ese hashtag.</string>
<string name="add_hashtag_label_field">Hashtag</string>
<string name="add_hashtag_label_confirm">Añadir</string>
<string name="hashtag_label_added_by">añadido por</string>
<string name="pinned_notes">Notas fijadas</string>
@@ -941,6 +944,7 @@
<string name="interest_set_name_placeholder">Mis intereses</string>
<string name="interest_set_hashtag_add_placeholder">Añadir hashtag</string>
<string name="interest_set_hashtag_private_toggle">Privado</string>
<string name="interest_set_hashtag_count">%1$d hashtag(s)</string>
<string name="interest_set_actions_dialog_title">Acciones del conjunto de intereses</string>
<string name="interest_set_rename">Renombrar</string>
<string name="interest_set_clone">Clonar</string>
@@ -980,6 +984,7 @@
<string name="quick_zap_amounts_explainer">Aparece al presionar el botón de zap. Pulsa una cantidad para eliminarla. Si la dejas vacía, se abrirá el cuadro de diálogo para introducir una cantidad cada vez.</string>
<string name="send_onchain_instead">Enviar en cadena en su lugar</string>
<string name="reload_mint_title">Recargar mint</string>
<string name="reload_mint_sats_amount">%1$s sats</string>
<string name="reload_mint_topup_label">Importe de recarga</string>
<string name="reload_mint_section_to">Mint a recargar</string>
<string name="reload_mint_section_from">Fondos de</string>
@@ -1297,6 +1302,7 @@
<string name="hide_community_rules_violations_explainer">Elimina publicaciones de los feeds de comunidades cuando la comunidad publica un documento de normas NIP-9B y un evento no lo cumple. No tiene efecto cuando una comunidad no tiene normas estructuradas.</string>
<string name="security_section_filtering_preferences">Preferencias de filtrado</string>
<string name="security_section_blocked_content">Contenido bloqueado</string>
<string name="security_unlimited"></string>
<string name="security_blocked_users_empty">Aún no has bloqueado a ningún usuario.</string>
<string name="security_spamming_users_empty">Ninguna cuenta ha sido marcada como spam en esta sesión.</string>
<string name="security_hidden_words_empty">No hay palabras ocultas. Añade una palabra a continuación para ocultar las publicaciones que la contengan.</string>
@@ -1355,6 +1361,7 @@
<string name="relay_count_subscriptions">Suscripciones COUNT (%1$d)</string>
<string name="relay_outbox_events">Eventos de salida (%1$d)</string>
<string name="relay_filter_authors">%1$d autores</string>
<string name="relay_filter_ids">%1$d ids</string>
<string name="relay_filter_since">desde %1$s</string>
<string name="relay_filter_until">hasta %1$s</string>
<string name="relay_filter_limit">límite %1$d</string>
@@ -1365,6 +1372,7 @@
<string name="accepts_up_to">Acepta hasta</string>
<string name="time_in_the_future">%1$s en el futuro</string>
<string name="time_in_the_past">hace %1$s</string>
<string name="amount_in_bits">%1$s bits</string>
<string name="event_retention">Retención de eventos</string>
<string name="content_size">Tamaño de contenido</string>
<string name="connectivity">Conectividad</string>
@@ -1378,6 +1386,7 @@
<string name="max_limit_events_returning">Límite máximo (devolución de eventos)</string>
<string name="max_subid_length">Longitud máxima de subID</string>
<string name="cashu">Token de Cashu</string>
<string name="cashu_mint_label">Mint: %1$s</string>
<string name="cashu_redeem">Canjear</string>
<string name="cashu_copy_token">Copiar token</string>
<string name="quick_actions_open_in_another_app">Abrir en otra app</string>
@@ -1735,6 +1744,7 @@
<string name="wallet_outgoing">Enviado</string>
<string name="wallet_refresh">Actualizar</string>
<string name="wallet_filter_all">Todos</string>
<string name="wallet_filter_zaps">Zaps</string>
<string name="wallet_filter_non_zaps">No-Zaps</string>
<string name="wallet_add">Añadir cartera</string>
<string name="wallet_default">Predeterminada</string>
@@ -1758,10 +1768,13 @@
<string name="cashu_discovering">Buscando tu monedero…</string>
<string name="cashu_discovering_description">Los monederos NIP-60 se sincronizan entre clientes. Si creaste uno en otra aplicación con esta clave Nostr, debería aparecer en unos segundos.</string>
<string name="cashu_balance">Saldo</string>
<string name="cashu_mints">Mints</string>
<string name="cashu_mint_url">URL del mint</string>
<string name="cashu_remove_mint">Eliminar mint</string>
<string name="cashu_add_mint">Añadir mint</string>
<string name="cashu_history">Historial</string>
<string name="cashu_wallet_autosaves">Tu monedero guarda automáticamente a medida que añades o eliminas mints. Se crea una clave nutzap la primera vez que añades una mint.</string>
<string name="cashu_wallet_saving">Guardando…</string>
<string name="cashu_p2pk_section">Clave nutzap (avanzado)</string>
<string name="cashu_p2pk_explainer">Una clave privada separada usada únicamente para recibir nutzaps NIP-61. No es tu clave de identidad Nostr.</string>
<string name="cashu_p2pk_autogen">Generar una nueva clave</string>
@@ -1834,7 +1847,9 @@
<string name="cashu_quote_confirm">¿Pagar %1$s sat + hasta %2$s sat en comisiones?</string>
<string name="cashu_verify">Verificar</string>
<string name="cashu_mint_reachable">✓ El mint es accesible</string>
<string name="cashu_mint_reachable_named">✓ %1$s</string>
<string name="cashu_mint_unreachable">No se pudo alcanzar el mint: %1$s</string>
<string name="nutzap">Nutzap</string>
<string name="nutzap_failed_title">Nutzap fallido</string>
<string name="nutzap_failed_no_recipient">No hay clave pública de destinatario en la nota</string>
<string name="nutzap_failed_private_note">Los nutzaps son públicos y revelarían esta nota privada. Usa un zap Lightning en su lugar.</string>
@@ -1876,12 +1891,18 @@
<string name="onchain_send_recipient_placeholder">nombre, NIP-05 o npub</string>
<string name="onchain_send_change_recipient">Cambiar destinatario</string>
<string name="onchain_send_amount">Cantidad</string>
<string name="onchain_send_sats_suffix">sats</string>
<string name="onchain_send_min_warning">El zap onchain mínimo es %1$s sats — las cantidades menores son consumidas por las comisiones de minería. Usa un zap Lightning en su lugar.</string>
<string name="onchain_send_comment_label">Comentario (opcional)</string>
<string name="onchain_send_priority">Prioridad</string>
<string name="onchain_send_fee_slow">Lento</string>
<string name="onchain_send_fee_normal">Normal</string>
<string name="onchain_send_fee_fast">Rápido</string>
<string name="onchain_send_fee_eta_slow">~1 h</string>
<string name="onchain_send_fee_eta_normal">~30 min</string>
<string name="onchain_send_fee_eta_fast">~10 min</string>
<string name="onchain_send_fee_rate_eta">%1$s sat/vB · %2$s</string>
<string name="onchain_send_fee_tier_label_rate_eta">%1$s · %2$s sat/vB · %3$s</string>
<string name="onchain_send_loading_fees">Cargando estimaciones de comisión…</string>
<string name="onchain_send_button_amount">Enviar %1$s sats</string>
<plurals name="onchain_send_button_amount_split">
@@ -1906,6 +1927,7 @@
<string name="onchain_send_result_transaction">Transacción</string>
<string name="onchain_send_result_fee">Comisión</string>
<string name="onchain_send_result_change">Cambio</string>
<string name="onchain_send_sats_amount">%1$s sats</string>
<string name="onchain_send_done">Hecho</string>
<string name="onchain_send_failed_at">Error en: %1$s</string>
<string name="onchain_send_broadcast_no_receipt">El pago se difundió (tx %1$s) pero el recibo no se publicó.</string>
@@ -1940,6 +1962,7 @@
<string name="edit_calendar_collection">Editar calendario</string>
<string name="calendar_collection_events_section">Eventos en este calendario (%1$d)</string>
<string name="calendar_collection_no_events_yet">Aún no has creado ningún evento de calendario.</string>
<string name="calendar_view_feed">Tablón</string>
<string name="calendar_view_month">Mes</string>
<string name="calendar_view_week">Semana</string>
<string name="calendar_view_day">Día</string>
@@ -2001,6 +2024,7 @@
<string name="calendar_rsvp_going">Asistiré</string>
<string name="calendar_rsvp_maybe">Tal vez</string>
<string name="calendar_rsvp_not_going">No puedo ir</string>
<string name="calendar_rsvp_section">Confirmaciones (%1$d)</string>
<string name="calendar_rsvp_none">Sin RSVPs aún.</string>
<string name="calendar_participants_section">Participantes (%1$d)</string>
<string name="calendar_event_in_calendars">En calendarios (%1$d)</string>
@@ -2010,6 +2034,7 @@
<string name="calendar_relative_ongoing_with_end">%1$s · termina %2$s</string>
<string name="calendar_export_share_title">Compartir evento de calendario</string>
<string name="calendar_export_event">Exportar al calendario (.ics)</string>
<string name="calendar_reminder_channel_id">recordatorios_calendario</string>
<string name="calendar_reminder_channel_name">Recordatorios de calendario</string>
<string name="calendar_reminder_channel_description">Aviso cuando un evento al que asistirás está a punto de comenzar.</string>
<string name="calendar_reminder_default_title">Evento de calendario</string>
@@ -2034,6 +2059,10 @@
<string name="calendar_reminder_settings_enabled_subtitle">Se enviará una notificación cuando un evento al que asistirás esté a punto de comenzar.</string>
<string name="calendar_reminder_settings_lead_title">Tiempo de antelación del recordatorio</string>
<string name="calendar_reminder_settings_lead_subtitle">Cuántos minutos antes del evento quieres recibir la notificación.</string>
<plurals name="calendar_reminder_settings_lead_choice">
<item quantity="one">%1$d min</item>
<item quantity="other">%1$d min</item>
</plurals>
<string name="calendar_share_nostr">Compartir como enlace Nostr</string>
<string name="calendar_share_nostr_title">Compartir enlace de calendario</string>
<string name="calendar_filter_all">Todos los calendarios</string>
@@ -2182,6 +2211,7 @@
<string name="private_inbox_section">Relés de entrada DM</string>
<string name="private_inbox_section_explainer_profile">El usuario recibe mensajes directos en estos relés</string>
<string name="private_inbox_section_explainer">Inserta entre 1 y 3 relés para que te sirvan de buzón de entrada privado. Otros usarán estos relés para enviarte mensajes directos. Los relés del buzón de entrada para mensajes directos deberían aceptar cualquier mensaje de cualquier persona, pero solo te permiten descargarlos. Algunas buenas opciones son:\n - inbox.nostr.wine (de pago)\n - you.nostr1.com (relés personales; de pago)</string>
<string name="keypackages">Paquetes de claves</string>
<string name="keypackage_section">Relays de KeyPackage</string>
<string name="keypackage_section_explainer">Relays donde se publican tus MLS KeyPackages (MIP-00). Otros usuarios obtienen estas KeyPackages para invitarte a chats de grupo Marmot. Introduce entre 1 y 3 relays que acepten eventos KeyPackage de tu parte y permitan lecturas públicas.</string>
<string name="private_outbox_section">Relés privados</string>
@@ -2226,17 +2256,27 @@
<string name="forked_from">Bifurcación de</string>
<string name="git_web_address">Web:</string>
<string name="git_clone_address">Clon:</string>
<string name="git_branch">Rama</string>
<string name="git_commit">Confirmar</string>
<string name="git_merge_base">Fusionar base</string>
<string name="git_pr_update_description">Se actualizó la solicitud de incorporación de cambios con una nueva confirmación.</string>
<string name="git_status_open">Abierto</string>
<string name="git_status_merged">Fusionado</string>
<string name="git_status_closed">Cerrado</string>
<string name="git_status_draft">Borrador</string>
<string name="git_repo_tab_overview">Resumen</string>
<string name="git_repo_tab_issues">Incidencias</string>
<string name="git_repo_tab_patches">Parches y PRs</string>
<string name="git_repo_filter_open">Abierto</string>
<string name="git_repo_filter_closed">Cerrado y resuelto</string>
<string name="git_untitled">Sin título</string>
<string name="git_repo_section_about">Acerca de</string>
<string name="git_repo_section_links">Enlaces</string>
<string name="git_repo_section_maintainers">Mantenedores</string>
<string name="git_repo_section_topics">Temas</string>
<string name="git_repo_personal_fork">Fork personal</string>
<string name="nsite_title">nSite: %1$s</string>
<string name="napplet_card_title">nApplet: %1$s</string>
<string name="napplet_card_permissions">Permisos:</string>
<string name="profile_tab_apps">Apps y sitios</string>
<string name="nsite_root_site">Sitio raíz</string>
@@ -2310,12 +2350,15 @@
<string name="torrent_no_info">El evento no tiene suficiente información para crear un enlace magnético</string>
<string name="my_lists">Mis listas</string>
<string name="select_list_to_filter">Selecciona una lista para filtrar el tablón</string>
<string name="feed_group_feeds">Tablones</string>
<string name="feed_group_hashtags">Hashtags</string>
<string name="feed_group_interest_sets">Conjuntos de intereses</string>
<string name="feed_group_locations">Ubicaciones</string>
<string name="feed_group_communities">Comunidades</string>
<string name="feed_group_lists">Listas</string>
<string name="feed_group_dvms">Algoritmos de feed</string>
<string name="follow_list_all_favorite_dvms">Todos los algoritmos de feed favoritos</string>
<string name="feed_group_relays">Relés</string>
<string name="add_dvm_to_favorites">Añadir algoritmo de feed a favoritos</string>
<string name="remove_dvm_from_favorites">Eliminar de favoritos</string>
<string name="favorite_dvms_title">Algoritmos de feed favoritos</string>
@@ -2345,10 +2388,13 @@
<string name="search_scope_all">Todo</string>
<string name="search_scope_people">Personas</string>
<string name="search_scope_notes">Notas</string>
<string name="search_source_local">Local</string>
<string name="search_source_relays">Relés</string>
<string name="search_follows_only">Solo seguidos</string>
<string name="search_sort_newest">Más recientes</string>
<string name="search_sort_oldest">Más antiguos</string>
<string name="search_sort_relevance">Relevancia</string>
<string name="search_sort_popular">Popular</string>
<string name="search_filters_title">Filtros</string>
<string name="search_filters_reset">Restablecer</string>
<string name="search_filters_section_source">Fuente</string>
@@ -2360,6 +2406,7 @@
<string name="translate_to_description">Elige el idioma al que traducir el contenido.</string>
<string name="language_preferences">Preferencias de visualización de idioma</string>
<string name="language_preferences_description">Para cada par de idiomas traducidos, elige qué idioma mostrar primero.</string>
<string name="language_preference_pair">%1$s → %2$s</string>
<string name="search_languages">Buscar idiomas</string>
<string name="add_language">Añadir idioma</string>
<string name="add_language_pair">Añadir par de idiomas</string>
@@ -2416,6 +2463,7 @@
<string name="retrying">Reintentando…</string>
<string name="retry">Reintentar</string>
<string name="dismiss">Descartar</string>
<string name="share_of">%1$d/%2$d</string>
<string name="broadcasting">Difundiendo</string>
<string name="broadcasting_name">Difundiendo %1$s</string>
<string name="broadcasting_number_events">Difundiendo %1$d eventos…</string>
@@ -2512,10 +2560,13 @@
<string name="kind_follow_packs">Packs de seguidos</string>
<string name="kind_reposts_16">Republicaciones (16)</string>
<string name="kind_geohash_follows">Seguidos por geohash</string>
<string name="kind_gift_wraps">GiftWraps</string>
<string name="kind_git_issue">Incidencia de Git</string>
<string name="kind_git_patch">Parche de Git</string>
<string name="kind_git_repo">Repositorio de Git</string>
<string name="kind_git_reply">Respuesta de Git</string>
<string name="kind_git_pr">Solicitud de incorporación</string>
<string name="kind_git_pr_update">Actualización de solicitud de incorporación</string>
<string name="kind_zap_goals">Objetivos de Zap</string>
<string name="kind_hashtag_follows">Seguidos por hashtag</string>
<string name="kind_highlights">Destacados</string>
@@ -2527,20 +2578,27 @@
<string name="kind_named_bookmarks">Marcadores con nombre</string>
<string name="kind_live_chats">Chats en directo</string>
<string name="kind_live_streams">Transmisiones en directo</string>
<string name="kind_zaps">Zaps</string>
<string name="kind_nwc_request">Solicitud NWC</string>
<string name="kind_nwc_response">Respuesta NWC</string>
<string name="kind_private_zaps">Zaps privados</string>
<string name="kind_zap_req">Solicitud de Zap</string>
<string name="kind_blogs">Blogs</string>
<string name="kind_meeting_room">Sala de reuniones</string>
<string name="kind_room_presence">Presencia en sala</string>
<string name="kind_meeting_space">Espacio de reuniones</string>
<string name="kind_profile">Perfil</string>
<string name="kind_mute_list">Lista de silenciados</string>
<string name="kind_nns">NNS</string>
<string name="kind_nip">NIP</string>
<string name="kind_nostr_connect">Nostr Connect</string>
<string name="kind_dvm_status">Estado DVM</string>
<string name="kind_dvm_content_req">Solicitud de contenido DVM</string>
<string name="kind_dvm_content_resp">Respuesta de contenido DVM</string>
<string name="kind_dvm_user_req">Solicitud de usuario DVM</string>
<string name="kind_dvm_user_resp">Respuesta de usuario DVM</string>
<string name="kind_ots">OTS</string>
<string name="kind_pay_to">Pagar a</string>
<string name="kind_people_lists">Listas de personas</string>
<string name="kind_pictures">Imágenes</string>
<string name="kind_workouts">Entrenamientos</string>
@@ -2548,6 +2606,7 @@
<string name="kind_zap_poll">Encuesta de Zap</string>
<string name="kind_poll">Encuesta</string>
<string name="kind_poll_response">Respuesta de encuesta</string>
<string name="kind_nip04_dms">Mensajes directos NIP-04</string>
<string name="kind_private_relays">Relés privados</string>
<string name="kind_proxy_relays">Relés proxy</string>
<string name="kind_public_message">Mensaje público</string>
@@ -2565,6 +2624,7 @@
<string name="kind_user_status">Estado del usuario</string>
<string name="kind_notes">Notas</string>
<string name="kind_edits">Ediciones</string>
<string name="kind_torrents">Torrents</string>
<string name="kind_torrent_comments">Comentarios de torrent</string>
<string name="kind_trusted_relays">Relés de confianza</string>
<string name="kind_trusted_providers">Proveedores de confianza</string>
@@ -712,6 +712,7 @@
<item quantity="other">Este nApplet quiere pagar una factura Lightning de %1$d sats.</item>
</plurals>
<string name="nip82_repository_label">Fuente: %1$s</string>
<string name="nip82_version_label">v%1$s</string>
<string name="nip82_download">Descargar</string>
<string name="nip82_section_about">Acerca de</string>
<string name="nip82_section_platforms">Plataformas</string>
@@ -870,6 +871,7 @@
<string name="route_music_tracks">Música</string>
<string name="route_music_playlists">Listas de reproducción</string>
<string name="route_podcast_episodes">Episodios</string>
<string name="route_podcasts">Podcasts</string>
<string name="podcast_view_episodes">Ver episodios</string>
<string name="podcast_no_episodes">Aún no se han encontrado episodios</string>
<plurals name="podcast_episode_count">
@@ -907,6 +909,7 @@
<string name="add_hashtag_label">Añadir hashtag</string>
<string name="add_hashtag_label_title">Añadir un hashtag</string>
<string name="add_hashtag_label_explainer">Etiqueta públicamente esta publicación con un hashtag (etiqueta NIP-32). Las personas que te siguen lo verán en el feed de ese hashtag.</string>
<string name="add_hashtag_label_field">Hashtag</string>
<string name="add_hashtag_label_confirm">Añadir</string>
<string name="hashtag_label_added_by">añadido por</string>
<string name="pinned_notes">Notas fijadas</string>
@@ -933,6 +936,7 @@
<string name="interest_set_name_placeholder">Mis intereses</string>
<string name="interest_set_hashtag_add_placeholder">Añadir hashtag</string>
<string name="interest_set_hashtag_private_toggle">Privado</string>
<string name="interest_set_hashtag_count">%1$d hashtag(s)</string>
<string name="interest_set_actions_dialog_title">Acciones del conjunto de intereses</string>
<string name="interest_set_rename">Renombrar</string>
<string name="interest_set_clone">Clonar</string>
@@ -972,6 +976,7 @@
<string name="quick_zap_amounts_explainer">Aparece al presionar el botón de zap. Toca una cantidad para eliminarla. Si la dejas vacía, se abrirá el cuadro de diálogo para introducir una cantidad cada vez.</string>
<string name="send_onchain_instead">Enviar en cadena en su lugar</string>
<string name="reload_mint_title">Recargar mint</string>
<string name="reload_mint_sats_amount">%1$s sats</string>
<string name="reload_mint_topup_label">Importe de recarga</string>
<string name="reload_mint_section_to">Mint a recargar</string>
<string name="reload_mint_section_from">Fondos de</string>
@@ -1289,6 +1294,7 @@
<string name="hide_community_rules_violations_explainer">Elimina publicaciones de los feeds de comunidades cuando la comunidad publica un documento de normas NIP-9B y un evento no lo cumple. No tiene efecto cuando una comunidad no tiene normas estructuradas.</string>
<string name="security_section_filtering_preferences">Preferencias de filtrado</string>
<string name="security_section_blocked_content">Contenido bloqueado</string>
<string name="security_unlimited"></string>
<string name="security_blocked_users_empty">Aún no has bloqueado a ningún usuario.</string>
<string name="security_spamming_users_empty">Ninguna cuenta ha sido marcada como spam en esta sesión.</string>
<string name="security_hidden_words_empty">No hay palabras ocultas. Añade una palabra a continuación para ocultar las publicaciones que la contengan.</string>
@@ -1347,6 +1353,7 @@
<string name="relay_count_subscriptions">Suscripciones COUNT (%1$d)</string>
<string name="relay_outbox_events">Eventos de salida (%1$d)</string>
<string name="relay_filter_authors">%1$d autores</string>
<string name="relay_filter_ids">%1$d ids</string>
<string name="relay_filter_since">desde %1$s</string>
<string name="relay_filter_until">hasta %1$s</string>
<string name="relay_filter_limit">límite %1$d</string>
@@ -1357,6 +1364,7 @@
<string name="accepts_up_to">Acepta hasta</string>
<string name="time_in_the_future">%1$s en el futuro</string>
<string name="time_in_the_past">hace %1$s</string>
<string name="amount_in_bits">%1$s bits</string>
<string name="event_retention">Retención de eventos</string>
<string name="content_size">Tamaño de contenido</string>
<string name="connectivity">Conectividad</string>
@@ -1370,6 +1378,7 @@
<string name="max_limit_events_returning">Límite máximo (devolución de eventos)</string>
<string name="max_subid_length">Longitud máxima de subID</string>
<string name="cashu">Token de Cashu</string>
<string name="cashu_mint_label">Mint: %1$s</string>
<string name="cashu_redeem">Canjear</string>
<string name="cashu_copy_token">Copiar token</string>
<string name="quick_actions_open_in_another_app">Abrir en otra app</string>
@@ -1727,6 +1736,7 @@
<string name="wallet_outgoing">Enviado</string>
<string name="wallet_refresh">Actualizar</string>
<string name="wallet_filter_all">Todos</string>
<string name="wallet_filter_zaps">Zaps</string>
<string name="wallet_filter_non_zaps">No-Zaps</string>
<string name="wallet_add">Añadir cartera</string>
<string name="wallet_default">Predeterminada</string>
@@ -1750,10 +1760,13 @@
<string name="cashu_discovering">Buscando tu monedero…</string>
<string name="cashu_discovering_description">Los monederos NIP-60 se sincronizan entre clientes. Si creaste uno en otra aplicación con esta clave Nostr, debería aparecer en unos segundos.</string>
<string name="cashu_balance">Saldo</string>
<string name="cashu_mints">Mints</string>
<string name="cashu_mint_url">URL del mint</string>
<string name="cashu_remove_mint">Eliminar mint</string>
<string name="cashu_add_mint">Añadir mint</string>
<string name="cashu_history">Historial</string>
<string name="cashu_wallet_autosaves">Tu billetera guarda automáticamente a medida que añades o eliminas mints. Se crea una clave nutzap la primera vez que añades una mint.</string>
<string name="cashu_wallet_saving">Guardando…</string>
<string name="cashu_p2pk_section">Clave nutzap (avanzado)</string>
<string name="cashu_p2pk_explainer">Una clave privada separada usada únicamente para recibir nutzaps NIP-61. No es tu clave de identidad Nostr.</string>
<string name="cashu_p2pk_autogen">Generar una nueva clave</string>
@@ -1826,7 +1839,9 @@
<string name="cashu_quote_confirm">¿Pagar %1$s sat + hasta %2$s sat en comisiones?</string>
<string name="cashu_verify">Verificar</string>
<string name="cashu_mint_reachable">✓ El mint es accesible</string>
<string name="cashu_mint_reachable_named">✓ %1$s</string>
<string name="cashu_mint_unreachable">No se pudo alcanzar el mint: %1$s</string>
<string name="nutzap">Nutzap</string>
<string name="nutzap_failed_title">Nutzap fallido</string>
<string name="nutzap_failed_no_recipient">No hay clave pública de destinatario en la nota</string>
<string name="nutzap_failed_private_note">Los nutzaps son públicos y revelarían esta nota privada. Usa un zap Lightning en su lugar.</string>
@@ -1868,12 +1883,18 @@
<string name="onchain_send_recipient_placeholder">nombre, NIP-05 o npub</string>
<string name="onchain_send_change_recipient">Cambiar destinatario</string>
<string name="onchain_send_amount">Cantidad</string>
<string name="onchain_send_sats_suffix">sats</string>
<string name="onchain_send_min_warning">El zap onchain mínimo es %1$s sats — las cantidades menores son consumidas por las comisiones de minería. Usa un zap Lightning en su lugar.</string>
<string name="onchain_send_comment_label">Comentario (opcional)</string>
<string name="onchain_send_priority">Prioridad</string>
<string name="onchain_send_fee_slow">Lento</string>
<string name="onchain_send_fee_normal">Normal</string>
<string name="onchain_send_fee_fast">Rápido</string>
<string name="onchain_send_fee_eta_slow">~1 h</string>
<string name="onchain_send_fee_eta_normal">~30 min</string>
<string name="onchain_send_fee_eta_fast">~10 min</string>
<string name="onchain_send_fee_rate_eta">%1$s sat/vB · %2$s</string>
<string name="onchain_send_fee_tier_label_rate_eta">%1$s · %2$s sat/vB · %3$s</string>
<string name="onchain_send_loading_fees">Cargando estimaciones de comisión…</string>
<string name="onchain_send_button_amount">Enviar %1$s sats</string>
<plurals name="onchain_send_button_amount_split">
@@ -1898,6 +1919,7 @@
<string name="onchain_send_result_transaction">Transacción</string>
<string name="onchain_send_result_fee">Comisión</string>
<string name="onchain_send_result_change">Cambio</string>
<string name="onchain_send_sats_amount">%1$s sats</string>
<string name="onchain_send_done">Hecho</string>
<string name="onchain_send_failed_at">Error en: %1$s</string>
<string name="onchain_send_broadcast_no_receipt">El pago se difundió (tx %1$s) pero el recibo no se publicó.</string>
@@ -1932,6 +1954,7 @@
<string name="edit_calendar_collection">Editar calendario</string>
<string name="calendar_collection_events_section">Eventos en este calendario (%1$d)</string>
<string name="calendar_collection_no_events_yet">Aún no has creado ningún evento de calendario.</string>
<string name="calendar_view_feed">Tablón</string>
<string name="calendar_view_month">Mes</string>
<string name="calendar_view_week">Semana</string>
<string name="calendar_view_day">Día</string>
@@ -1993,6 +2016,7 @@
<string name="calendar_rsvp_going">Asistiré</string>
<string name="calendar_rsvp_maybe">Tal vez</string>
<string name="calendar_rsvp_not_going">No puedo ir</string>
<string name="calendar_rsvp_section">Confirmaciones (%1$d)</string>
<string name="calendar_rsvp_none">Sin RSVPs aún.</string>
<string name="calendar_participants_section">Participantes (%1$d)</string>
<string name="calendar_event_in_calendars">En calendarios (%1$d)</string>
@@ -2002,6 +2026,7 @@
<string name="calendar_relative_ongoing_with_end">%1$s · termina %2$s</string>
<string name="calendar_export_share_title">Compartir evento de calendario</string>
<string name="calendar_export_event">Exportar al calendario (.ics)</string>
<string name="calendar_reminder_channel_id">recordatorios_calendario</string>
<string name="calendar_reminder_channel_name">Recordatorios de calendario</string>
<string name="calendar_reminder_channel_description">Aviso cuando un evento al que asistirás está a punto de comenzar.</string>
<string name="calendar_reminder_default_title">Evento de calendario</string>
@@ -2026,6 +2051,10 @@
<string name="calendar_reminder_settings_enabled_subtitle">Se enviará una notificación cuando un evento al que asistirás esté a punto de comenzar.</string>
<string name="calendar_reminder_settings_lead_title">Tiempo de antelación del recordatorio</string>
<string name="calendar_reminder_settings_lead_subtitle">Cuántos minutos antes del evento quieres recibir la notificación.</string>
<plurals name="calendar_reminder_settings_lead_choice">
<item quantity="one">%1$d min</item>
<item quantity="other">%1$d min</item>
</plurals>
<string name="calendar_share_nostr">Compartir como enlace Nostr</string>
<string name="calendar_share_nostr_title">Compartir enlace de calendario</string>
<string name="calendar_filter_all">Todos los calendarios</string>
@@ -2174,6 +2203,7 @@
<string name="private_inbox_section">Relés de entrada DM</string>
<string name="private_inbox_section_explainer_profile">El usuario recibe mensajes directos en estos relés</string>
<string name="private_inbox_section_explainer">Inserta entre 1 y 3 relés para que te sirvan de buzón de entrada privado. Otros usarán estos relés para enviarte mensajes directos. Los relés del buzón de entrada para mensajes directos deberían aceptar cualquier mensaje de cualquier persona, pero solo te permiten descargarlos. Algunas buenas opciones son:\n - inbox.nostr.wine (de pago)\n - you.nostr1.com (relés personales; de pago)</string>
<string name="keypackages">Paquetes de claves</string>
<string name="keypackage_section">Relays de KeyPackage</string>
<string name="keypackage_section_explainer">Relays donde se publican tus MLS KeyPackages (MIP-00). Otros usuarios obtienen estas KeyPackages para invitarte a chats de grupo Marmot. Introduce entre 1 y 3 relays que acepten eventos KeyPackage de tu parte y permitan lecturas públicas.</string>
<string name="private_outbox_section">Relés privados</string>
@@ -2218,17 +2248,27 @@
<string name="forked_from">Bifurcación de</string>
<string name="git_web_address">Web:</string>
<string name="git_clone_address">Clon:</string>
<string name="git_branch">Rama</string>
<string name="git_commit">Confirmar</string>
<string name="git_merge_base">Fusionar base</string>
<string name="git_pr_update_description">Se actualizó la solicitud de incorporación de cambios con una nueva confirmación.</string>
<string name="git_status_open">Abierto</string>
<string name="git_status_merged">Fusionado</string>
<string name="git_status_closed">Cerrado</string>
<string name="git_status_draft">Borrador</string>
<string name="git_repo_tab_overview">Resumen</string>
<string name="git_repo_tab_issues">Incidencias</string>
<string name="git_repo_tab_patches">Parches y PRs</string>
<string name="git_repo_filter_open">Abierto</string>
<string name="git_repo_filter_closed">Cerrado y resuelto</string>
<string name="git_untitled">Sin título</string>
<string name="git_repo_section_about">Acerca de</string>
<string name="git_repo_section_links">Enlaces</string>
<string name="git_repo_section_maintainers">Mantenedores</string>
<string name="git_repo_section_topics">Temas</string>
<string name="git_repo_personal_fork">Fork personal</string>
<string name="nsite_title">nSite: %1$s</string>
<string name="napplet_card_title">nApplet: %1$s</string>
<string name="napplet_card_permissions">Permisos:</string>
<string name="profile_tab_apps">Apps y sitios</string>
<string name="nsite_root_site">Sitio raíz</string>
@@ -2302,12 +2342,15 @@
<string name="torrent_no_info">El evento no tiene suficiente información para crear un enlace magnético</string>
<string name="my_lists">Mis listas</string>
<string name="select_list_to_filter">Selecciona una lista para filtrar el feed</string>
<string name="feed_group_feeds">Tablones</string>
<string name="feed_group_hashtags">Hashtags</string>
<string name="feed_group_interest_sets">Conjuntos de intereses</string>
<string name="feed_group_locations">Ubicaciones</string>
<string name="feed_group_communities">Comunidades</string>
<string name="feed_group_lists">Listas</string>
<string name="feed_group_dvms">Algoritmos de feed</string>
<string name="follow_list_all_favorite_dvms">Todos los algoritmos de feed favoritos</string>
<string name="feed_group_relays">Relés</string>
<string name="add_dvm_to_favorites">Añadir algoritmo de feed a favoritos</string>
<string name="remove_dvm_from_favorites">Eliminar de favoritos</string>
<string name="favorite_dvms_title">Algoritmos de feed favoritos</string>
@@ -2337,10 +2380,13 @@
<string name="search_scope_all">Todo</string>
<string name="search_scope_people">Personas</string>
<string name="search_scope_notes">Notas</string>
<string name="search_source_local">Local</string>
<string name="search_source_relays">Relés</string>
<string name="search_follows_only">Solo seguidos</string>
<string name="search_sort_newest">Más recientes</string>
<string name="search_sort_oldest">Más antiguos</string>
<string name="search_sort_relevance">Relevancia</string>
<string name="search_sort_popular">Popular</string>
<string name="search_filters_title">Filtros</string>
<string name="search_filters_reset">Restablecer</string>
<string name="search_filters_section_source">Fuente</string>
@@ -2352,6 +2398,7 @@
<string name="translate_to_description">Elige el idioma al que traducir el contenido.</string>
<string name="language_preferences">Preferencias de visualización de idioma</string>
<string name="language_preferences_description">Para cada par de idiomas traducidos, elige qué idioma mostrar primero.</string>
<string name="language_preference_pair">%1$s → %2$s</string>
<string name="search_languages">Buscar idiomas</string>
<string name="add_language">Añadir idioma</string>
<string name="add_language_pair">Añadir par de idiomas</string>
@@ -2408,6 +2455,7 @@
<string name="retrying">Reintentando…</string>
<string name="retry">Reintentar</string>
<string name="dismiss">Descartar</string>
<string name="share_of">%1$d/%2$d</string>
<string name="broadcasting">Difundiendo</string>
<string name="broadcasting_name">Difundiendo %1$s</string>
<string name="broadcasting_number_events">Difundiendo %1$d eventos…</string>
@@ -2504,10 +2552,13 @@
<string name="kind_follow_packs">Packs de seguidos</string>
<string name="kind_reposts_16">Republicaciones (16)</string>
<string name="kind_geohash_follows">Seguidos por geohash</string>
<string name="kind_gift_wraps">GiftWraps</string>
<string name="kind_git_issue">Incidencia de Git</string>
<string name="kind_git_patch">Parche de Git</string>
<string name="kind_git_repo">Repositorio de Git</string>
<string name="kind_git_reply">Respuesta de Git</string>
<string name="kind_git_pr">Solicitud de incorporación</string>
<string name="kind_git_pr_update">Actualización de solicitud de incorporación</string>
<string name="kind_zap_goals">Objetivos de Zap</string>
<string name="kind_hashtag_follows">Seguidos por hashtag</string>
<string name="kind_highlights">Destacados</string>
@@ -2519,20 +2570,27 @@
<string name="kind_named_bookmarks">Marcadores con nombre</string>
<string name="kind_live_chats">Chats en directo</string>
<string name="kind_live_streams">Transmisiones en directo</string>
<string name="kind_zaps">Zaps</string>
<string name="kind_nwc_request">Solicitud NWC</string>
<string name="kind_nwc_response">Respuesta NWC</string>
<string name="kind_private_zaps">Zaps privados</string>
<string name="kind_zap_req">Solicitud de Zap</string>
<string name="kind_blogs">Blogs</string>
<string name="kind_meeting_room">Sala de reuniones</string>
<string name="kind_room_presence">Presencia en sala</string>
<string name="kind_meeting_space">Espacio de reuniones</string>
<string name="kind_profile">Perfil</string>
<string name="kind_mute_list">Lista de silenciados</string>
<string name="kind_nns">NNS</string>
<string name="kind_nip">NIP</string>
<string name="kind_nostr_connect">Nostr Connect</string>
<string name="kind_dvm_status">Estado DVM</string>
<string name="kind_dvm_content_req">Solicitud de contenido DVM</string>
<string name="kind_dvm_content_resp">Respuesta de contenido DVM</string>
<string name="kind_dvm_user_req">Solicitud de usuario DVM</string>
<string name="kind_dvm_user_resp">Respuesta de usuario DVM</string>
<string name="kind_ots">OTS</string>
<string name="kind_pay_to">Pagar a</string>
<string name="kind_people_lists">Listas de personas</string>
<string name="kind_pictures">Imágenes</string>
<string name="kind_workouts">Entrenamientos</string>
@@ -2540,6 +2598,7 @@
<string name="kind_zap_poll">Encuesta de Zap</string>
<string name="kind_poll">Encuesta</string>
<string name="kind_poll_response">Respuesta de encuesta</string>
<string name="kind_nip04_dms">Mensajes directos NIP-04</string>
<string name="kind_private_relays">Relés privados</string>
<string name="kind_proxy_relays">Relés proxy</string>
<string name="kind_public_message">Mensaje público</string>
@@ -2557,6 +2616,7 @@
<string name="kind_user_status">Estado del usuario</string>
<string name="kind_notes">Notas</string>
<string name="kind_edits">Ediciones</string>
<string name="kind_torrents">Torrents</string>
<string name="kind_torrent_comments">Comentarios de torrent</string>
<string name="kind_trusted_relays">Relés de confianza</string>
<string name="kind_trusted_providers">Proveedores de confianza</string>
@@ -712,6 +712,7 @@
<item quantity="other">Este nApplet quiere pagar una factura Lightning de %1$d sats.</item>
</plurals>
<string name="nip82_repository_label">Fuente: %1$s</string>
<string name="nip82_version_label">v%1$s</string>
<string name="nip82_download">Descargar</string>
<string name="nip82_section_about">Acerca de</string>
<string name="nip82_section_platforms">Plataformas</string>
@@ -870,6 +871,7 @@
<string name="route_music_tracks">Música</string>
<string name="route_music_playlists">Listas de reproducción</string>
<string name="route_podcast_episodes">Episodios</string>
<string name="route_podcasts">Podcasts</string>
<string name="podcast_view_episodes">Ver episodios</string>
<string name="podcast_no_episodes">Aún no se han encontrado episodios</string>
<plurals name="podcast_episode_count">
@@ -907,6 +909,7 @@
<string name="add_hashtag_label">Añadir hashtag</string>
<string name="add_hashtag_label_title">Añadir un hashtag</string>
<string name="add_hashtag_label_explainer">Etiqueta públicamente esta publicación con un hashtag (etiqueta NIP-32). Las personas que te siguen lo verán en el feed de ese hashtag.</string>
<string name="add_hashtag_label_field">Hashtag</string>
<string name="add_hashtag_label_confirm">Añadir</string>
<string name="hashtag_label_added_by">añadido por</string>
<string name="pinned_notes">Notas fijadas</string>
@@ -933,6 +936,7 @@
<string name="interest_set_name_placeholder">Mis intereses</string>
<string name="interest_set_hashtag_add_placeholder">Añadir hashtag</string>
<string name="interest_set_hashtag_private_toggle">Privado</string>
<string name="interest_set_hashtag_count">%1$d hashtag(s)</string>
<string name="interest_set_actions_dialog_title">Acciones del conjunto de intereses</string>
<string name="interest_set_rename">Renombrar</string>
<string name="interest_set_clone">Clonar</string>
@@ -972,6 +976,7 @@
<string name="quick_zap_amounts_explainer">Aparece al presionar el botón de zap. Toca una cantidad para eliminarla. Si la dejas vacía, se abrirá el cuadro de diálogo para introducir una cantidad cada vez.</string>
<string name="send_onchain_instead">Enviar en cadena en su lugar</string>
<string name="reload_mint_title">Recargar mint</string>
<string name="reload_mint_sats_amount">%1$s sats</string>
<string name="reload_mint_topup_label">Importe de recarga</string>
<string name="reload_mint_section_to">Mint a recargar</string>
<string name="reload_mint_section_from">Fondos de</string>
@@ -1289,6 +1294,7 @@
<string name="hide_community_rules_violations_explainer">Elimina publicaciones de los feeds de comunidades cuando la comunidad publica un documento de normas NIP-9B y un evento no lo cumple. No tiene efecto cuando una comunidad no tiene normas estructuradas.</string>
<string name="security_section_filtering_preferences">Preferencias de filtrado</string>
<string name="security_section_blocked_content">Contenido bloqueado</string>
<string name="security_unlimited"></string>
<string name="security_blocked_users_empty">Aún no has bloqueado a ningún usuario.</string>
<string name="security_spamming_users_empty">Ninguna cuenta ha sido marcada como spam en esta sesión.</string>
<string name="security_hidden_words_empty">No hay palabras ocultas. Añade una palabra a continuación para ocultar las publicaciones que la contengan.</string>
@@ -1347,6 +1353,7 @@
<string name="relay_count_subscriptions">Suscripciones COUNT (%1$d)</string>
<string name="relay_outbox_events">Eventos de salida (%1$d)</string>
<string name="relay_filter_authors">%1$d autores</string>
<string name="relay_filter_ids">%1$d ids</string>
<string name="relay_filter_since">desde %1$s</string>
<string name="relay_filter_until">hasta %1$s</string>
<string name="relay_filter_limit">límite %1$d</string>
@@ -1357,6 +1364,7 @@
<string name="accepts_up_to">Acepta hasta</string>
<string name="time_in_the_future">%1$s en el futuro</string>
<string name="time_in_the_past">hace %1$s</string>
<string name="amount_in_bits">%1$s bits</string>
<string name="event_retention">Retención de eventos</string>
<string name="content_size">Tamaño de contenido</string>
<string name="connectivity">Conectividad</string>
@@ -1370,6 +1378,7 @@
<string name="max_limit_events_returning">Límite máximo (devolución de eventos)</string>
<string name="max_subid_length">Longitud máxima de subID</string>
<string name="cashu">Token de Cashu</string>
<string name="cashu_mint_label">Mint: %1$s</string>
<string name="cashu_redeem">Canjear</string>
<string name="cashu_copy_token">Copiar token</string>
<string name="quick_actions_open_in_another_app">Abrir en otra app</string>
@@ -1727,6 +1736,7 @@
<string name="wallet_outgoing">Enviado</string>
<string name="wallet_refresh">Actualizar</string>
<string name="wallet_filter_all">Todos</string>
<string name="wallet_filter_zaps">Zaps</string>
<string name="wallet_filter_non_zaps">No-Zaps</string>
<string name="wallet_add">Añadir cartera</string>
<string name="wallet_default">Predeterminada</string>
@@ -1750,10 +1760,13 @@
<string name="cashu_discovering">Buscando tu monedero…</string>
<string name="cashu_discovering_description">Los monederos NIP-60 se sincronizan entre clientes. Si creaste uno en otra aplicación con esta clave Nostr, debería aparecer en unos segundos.</string>
<string name="cashu_balance">Saldo</string>
<string name="cashu_mints">Mints</string>
<string name="cashu_mint_url">URL del mint</string>
<string name="cashu_remove_mint">Eliminar mint</string>
<string name="cashu_add_mint">Añadir mint</string>
<string name="cashu_history">Historial</string>
<string name="cashu_wallet_autosaves">Tu billetera guarda automáticamente a medida que añades o eliminas mints. Se crea una clave nutzap la primera vez que añades una mint.</string>
<string name="cashu_wallet_saving">Guardando…</string>
<string name="cashu_p2pk_section">Clave nutzap (avanzado)</string>
<string name="cashu_p2pk_explainer">Una clave privada separada usada únicamente para recibir nutzaps NIP-61. No es tu clave de identidad Nostr.</string>
<string name="cashu_p2pk_autogen">Generar una nueva clave</string>
@@ -1826,7 +1839,9 @@
<string name="cashu_quote_confirm">¿Pagar %1$s sat + hasta %2$s sat en comisiones?</string>
<string name="cashu_verify">Verificar</string>
<string name="cashu_mint_reachable">✓ El mint es accesible</string>
<string name="cashu_mint_reachable_named">✓ %1$s</string>
<string name="cashu_mint_unreachable">No se pudo alcanzar el mint: %1$s</string>
<string name="nutzap">Nutzap</string>
<string name="nutzap_failed_title">Nutzap fallido</string>
<string name="nutzap_failed_no_recipient">No hay clave pública de destinatario en la nota</string>
<string name="nutzap_failed_private_note">Los nutzaps son públicos y revelarían esta nota privada. Usa un zap Lightning en su lugar.</string>
@@ -1868,12 +1883,18 @@
<string name="onchain_send_recipient_placeholder">nombre, NIP-05 o npub</string>
<string name="onchain_send_change_recipient">Cambiar destinatario</string>
<string name="onchain_send_amount">Cantidad</string>
<string name="onchain_send_sats_suffix">sats</string>
<string name="onchain_send_min_warning">El zap onchain mínimo es %1$s sats — las cantidades menores son consumidas por las comisiones de minería. Usa un zap Lightning en su lugar.</string>
<string name="onchain_send_comment_label">Comentario (opcional)</string>
<string name="onchain_send_priority">Prioridad</string>
<string name="onchain_send_fee_slow">Lento</string>
<string name="onchain_send_fee_normal">Normal</string>
<string name="onchain_send_fee_fast">Rápido</string>
<string name="onchain_send_fee_eta_slow">~1 h</string>
<string name="onchain_send_fee_eta_normal">~30 min</string>
<string name="onchain_send_fee_eta_fast">~10 min</string>
<string name="onchain_send_fee_rate_eta">%1$s sat/vB · %2$s</string>
<string name="onchain_send_fee_tier_label_rate_eta">%1$s · %2$s sat/vB · %3$s</string>
<string name="onchain_send_loading_fees">Cargando estimaciones de comisión…</string>
<string name="onchain_send_button_amount">Enviar %1$s sats</string>
<plurals name="onchain_send_button_amount_split">
@@ -1898,6 +1919,7 @@
<string name="onchain_send_result_transaction">Transacción</string>
<string name="onchain_send_result_fee">Comisión</string>
<string name="onchain_send_result_change">Cambio</string>
<string name="onchain_send_sats_amount">%1$s sats</string>
<string name="onchain_send_done">Hecho</string>
<string name="onchain_send_failed_at">Error en: %1$s</string>
<string name="onchain_send_broadcast_no_receipt">El pago se difundió (tx %1$s) pero el recibo no se publicó.</string>
@@ -1932,6 +1954,7 @@
<string name="edit_calendar_collection">Editar calendario</string>
<string name="calendar_collection_events_section">Eventos en este calendario (%1$d)</string>
<string name="calendar_collection_no_events_yet">Aún no has creado ningún evento de calendario.</string>
<string name="calendar_view_feed">Tablón</string>
<string name="calendar_view_month">Mes</string>
<string name="calendar_view_week">Semana</string>
<string name="calendar_view_day">Día</string>
@@ -1993,6 +2016,7 @@
<string name="calendar_rsvp_going">Asistiré</string>
<string name="calendar_rsvp_maybe">Tal vez</string>
<string name="calendar_rsvp_not_going">No puedo ir</string>
<string name="calendar_rsvp_section">Confirmaciones (%1$d)</string>
<string name="calendar_rsvp_none">Sin RSVPs aún.</string>
<string name="calendar_participants_section">Participantes (%1$d)</string>
<string name="calendar_event_in_calendars">En calendarios (%1$d)</string>
@@ -2002,6 +2026,7 @@
<string name="calendar_relative_ongoing_with_end">%1$s · termina %2$s</string>
<string name="calendar_export_share_title">Compartir evento de calendario</string>
<string name="calendar_export_event">Exportar al calendario (.ics)</string>
<string name="calendar_reminder_channel_id">recordatorios_calendario</string>
<string name="calendar_reminder_channel_name">Recordatorios de calendario</string>
<string name="calendar_reminder_channel_description">Aviso cuando un evento al que asistirás está a punto de comenzar.</string>
<string name="calendar_reminder_default_title">Evento de calendario</string>
@@ -2026,6 +2051,10 @@
<string name="calendar_reminder_settings_enabled_subtitle">Se enviará una notificación cuando un evento al que asistirás esté a punto de comenzar.</string>
<string name="calendar_reminder_settings_lead_title">Tiempo de antelación del recordatorio</string>
<string name="calendar_reminder_settings_lead_subtitle">Cuántos minutos antes del evento quieres recibir la notificación.</string>
<plurals name="calendar_reminder_settings_lead_choice">
<item quantity="one">%1$d min</item>
<item quantity="other">%1$d min</item>
</plurals>
<string name="calendar_share_nostr">Compartir como enlace Nostr</string>
<string name="calendar_share_nostr_title">Compartir enlace de calendario</string>
<string name="calendar_filter_all">Todos los calendarios</string>
@@ -2174,6 +2203,7 @@
<string name="private_inbox_section">Relés de entrada DM</string>
<string name="private_inbox_section_explainer_profile">El usuario recibe mensajes directos en estos relés</string>
<string name="private_inbox_section_explainer">Inserta entre 1 y 3 relés para que te sirvan de buzón de entrada privado. Otros usarán estos relés para enviarte mensajes directos. Los relés del buzón de entrada para mensajes directos deberían aceptar cualquier mensaje de cualquier persona, pero solo te permiten descargarlos. Algunas buenas opciones son:\n - inbox.nostr.wine (de pago)\n - you.nostr1.com (relés personales; de pago)</string>
<string name="keypackages">Paquetes de claves</string>
<string name="keypackage_section">Relays de KeyPackage</string>
<string name="keypackage_section_explainer">Relays donde se publican tus MLS KeyPackages (MIP-00). Otros usuarios obtienen estas KeyPackages para invitarte a chats de grupo Marmot. Introduce entre 1 y 3 relays que acepten eventos KeyPackage de tu parte y permitan lecturas públicas.</string>
<string name="private_outbox_section">Relés privados</string>
@@ -2218,17 +2248,27 @@
<string name="forked_from">Bifurcación de</string>
<string name="git_web_address">Web:</string>
<string name="git_clone_address">Clon:</string>
<string name="git_branch">Rama</string>
<string name="git_commit">Confirmar</string>
<string name="git_merge_base">Fusionar base</string>
<string name="git_pr_update_description">Se actualizó la solicitud de incorporación de cambios con una nueva confirmación.</string>
<string name="git_status_open">Abierto</string>
<string name="git_status_merged">Fusionado</string>
<string name="git_status_closed">Cerrado</string>
<string name="git_status_draft">Borrador</string>
<string name="git_repo_tab_overview">Resumen</string>
<string name="git_repo_tab_issues">Incidencias</string>
<string name="git_repo_tab_patches">Parches y PRs</string>
<string name="git_repo_filter_open">Abierto</string>
<string name="git_repo_filter_closed">Cerrado y resuelto</string>
<string name="git_untitled">Sin título</string>
<string name="git_repo_section_about">Acerca de</string>
<string name="git_repo_section_links">Enlaces</string>
<string name="git_repo_section_maintainers">Mantenedores</string>
<string name="git_repo_section_topics">Temas</string>
<string name="git_repo_personal_fork">Fork personal</string>
<string name="nsite_title">nSite: %1$s</string>
<string name="napplet_card_title">nApplet: %1$s</string>
<string name="napplet_card_permissions">Permisos:</string>
<string name="profile_tab_apps">Apps y sitios</string>
<string name="nsite_root_site">Sitio raíz</string>
@@ -2302,12 +2342,15 @@
<string name="torrent_no_info">El evento no tiene suficiente información para crear un enlace magnético</string>
<string name="my_lists">Mis listas</string>
<string name="select_list_to_filter">Selecciona una lista para filtrar el feed</string>
<string name="feed_group_feeds">Tablones</string>
<string name="feed_group_hashtags">Hashtags</string>
<string name="feed_group_interest_sets">Conjuntos de intereses</string>
<string name="feed_group_locations">Ubicaciones</string>
<string name="feed_group_communities">Comunidades</string>
<string name="feed_group_lists">Listas</string>
<string name="feed_group_dvms">Algoritmos de feed</string>
<string name="follow_list_all_favorite_dvms">Todos los algoritmos de feed favoritos</string>
<string name="feed_group_relays">Relés</string>
<string name="add_dvm_to_favorites">Añadir algoritmo de feed a favoritos</string>
<string name="remove_dvm_from_favorites">Eliminar de favoritos</string>
<string name="favorite_dvms_title">Algoritmos de feed favoritos</string>
@@ -2337,10 +2380,13 @@
<string name="search_scope_all">Todo</string>
<string name="search_scope_people">Personas</string>
<string name="search_scope_notes">Notas</string>
<string name="search_source_local">Local</string>
<string name="search_source_relays">Relés</string>
<string name="search_follows_only">Solo seguidos</string>
<string name="search_sort_newest">Más recientes</string>
<string name="search_sort_oldest">Más antiguos</string>
<string name="search_sort_relevance">Relevancia</string>
<string name="search_sort_popular">Popular</string>
<string name="search_filters_title">Filtros</string>
<string name="search_filters_reset">Restablecer</string>
<string name="search_filters_section_source">Fuente</string>
@@ -2352,6 +2398,7 @@
<string name="translate_to_description">Elige el idioma al que traducir el contenido.</string>
<string name="language_preferences">Preferencias de visualización de idioma</string>
<string name="language_preferences_description">Para cada par de idiomas traducidos, elige qué idioma mostrar primero.</string>
<string name="language_preference_pair">%1$s → %2$s</string>
<string name="search_languages">Buscar idiomas</string>
<string name="add_language">Añadir idioma</string>
<string name="add_language_pair">Añadir par de idiomas</string>
@@ -2408,6 +2455,7 @@
<string name="retrying">Reintentando…</string>
<string name="retry">Reintentar</string>
<string name="dismiss">Descartar</string>
<string name="share_of">%1$d/%2$d</string>
<string name="broadcasting">Difundiendo</string>
<string name="broadcasting_name">Difundiendo %1$s</string>
<string name="broadcasting_number_events">Difundiendo %1$d eventos…</string>
@@ -2504,10 +2552,13 @@
<string name="kind_follow_packs">Packs de seguidos</string>
<string name="kind_reposts_16">Republicaciones (16)</string>
<string name="kind_geohash_follows">Seguidos por geohash</string>
<string name="kind_gift_wraps">GiftWraps</string>
<string name="kind_git_issue">Incidencia de Git</string>
<string name="kind_git_patch">Parche de Git</string>
<string name="kind_git_repo">Repositorio de Git</string>
<string name="kind_git_reply">Respuesta de Git</string>
<string name="kind_git_pr">Solicitud de incorporación</string>
<string name="kind_git_pr_update">Actualización de solicitud de incorporación</string>
<string name="kind_zap_goals">Objetivos de Zap</string>
<string name="kind_hashtag_follows">Seguidos por hashtag</string>
<string name="kind_highlights">Destacados</string>
@@ -2519,20 +2570,27 @@
<string name="kind_named_bookmarks">Marcadores con nombre</string>
<string name="kind_live_chats">Chats en directo</string>
<string name="kind_live_streams">Transmisiones en directo</string>
<string name="kind_zaps">Zaps</string>
<string name="kind_nwc_request">Solicitud NWC</string>
<string name="kind_nwc_response">Respuesta NWC</string>
<string name="kind_private_zaps">Zaps privados</string>
<string name="kind_zap_req">Solicitud de Zap</string>
<string name="kind_blogs">Blogs</string>
<string name="kind_meeting_room">Sala de reuniones</string>
<string name="kind_room_presence">Presencia en sala</string>
<string name="kind_meeting_space">Espacio de reuniones</string>
<string name="kind_profile">Perfil</string>
<string name="kind_mute_list">Lista de silenciados</string>
<string name="kind_nns">NNS</string>
<string name="kind_nip">NIP</string>
<string name="kind_nostr_connect">Nostr Connect</string>
<string name="kind_dvm_status">Estado DVM</string>
<string name="kind_dvm_content_req">Solicitud de contenido DVM</string>
<string name="kind_dvm_content_resp">Respuesta de contenido DVM</string>
<string name="kind_dvm_user_req">Solicitud de usuario DVM</string>
<string name="kind_dvm_user_resp">Respuesta de usuario DVM</string>
<string name="kind_ots">OTS</string>
<string name="kind_pay_to">Pagar a</string>
<string name="kind_people_lists">Listas de personas</string>
<string name="kind_pictures">Imágenes</string>
<string name="kind_workouts">Entrenamientos</string>
@@ -2540,6 +2598,7 @@
<string name="kind_zap_poll">Encuesta de Zap</string>
<string name="kind_poll">Encuesta</string>
<string name="kind_poll_response">Respuesta de encuesta</string>
<string name="kind_nip04_dms">Mensajes directos NIP-04</string>
<string name="kind_private_relays">Relés privados</string>
<string name="kind_proxy_relays">Relés proxy</string>
<string name="kind_public_message">Mensaje público</string>
@@ -2557,6 +2616,7 @@
<string name="kind_user_status">Estado del usuario</string>
<string name="kind_notes">Notas</string>
<string name="kind_edits">Ediciones</string>
<string name="kind_torrents">Torrents</string>
<string name="kind_torrent_comments">Comentarios de torrent</string>
<string name="kind_trusted_relays">Relés de confianza</string>
<string name="kind_trusted_providers">Proveedores de confianza</string>
+1
View File
@@ -2580,6 +2580,7 @@
<string name="git_repo_section_maintainers">Maintainers</string>
<string name="git_repo_section_topics">Topics</string>
<string name="git_repo_personal_fork">Personal fork</string>
<string name="git_repositories">Git Repositories</string>
<string name="nsite_title">nSite: %1$s</string>
<string name="napplet_card_title">nApplet: %1$s</string>
<string name="napplet_card_permissions">Permissions:</string>
+8 -6
View File
@@ -145,6 +145,14 @@
"Slovenian"
]
},
{
"user": "BitByBit21",
"languages": [
"Spanish",
"Spanish, Mexico",
"Spanish, United States"
]
},
{
"user": "hypnotichemionus4",
"languages": [
@@ -157,12 +165,6 @@
"Hungarian"
]
},
{
"user": "BitByBit21",
"languages": [
"Spanish"
]
},
{
"user": "greenart7c3",
"languages": []