diff --git a/amethyst/build.gradle.kts b/amethyst/build.gradle.kts index 4088c67fd3..ae681168a5 100644 --- a/amethyst/build.gradle.kts +++ b/amethyst/build.gradle.kts @@ -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")) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 9b6a2abe3f..22ded89efe 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -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), diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 9f214f91b9..56ad5aad15 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -543,6 +543,9 @@ class Account( val liveWorkoutsFollowLists: StateFlow = topNavFilterFlow(settings.defaultWorkoutsFollowList) val liveWorkoutsFollowListsPerRelay = OutboxLoaderState(liveWorkoutsFollowLists, cache, scope).flow + val liveGitRepositoriesFollowLists: StateFlow = topNavFilterFlow(settings.defaultGitRepositoriesFollowList) + val liveGitRepositoriesFollowListsPerRelay = OutboxLoaderState(liveGitRepositoriesFollowLists, cache, scope).flow + val liveCalendarsFollowLists: StateFlow = topNavFilterFlow(settings.defaultCalendarsFollowList) val liveCalendarsFollowListsPerRelay = OutboxLoaderState(liveCalendarsFollowLists, cache, scope).flow diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt index 9a539ca5b3..a9b0a91781 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/AccountSettings.kt @@ -192,6 +192,7 @@ class AccountSettings( val defaultNappletsFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), val defaultNsitesFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), val defaultWorkoutsFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), + val defaultGitRepositoriesFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), val defaultCalendarsFollowList: MutableStateFlow = MutableStateFlow(TopFilter.Global), val defaultProductsFollowList: MutableStateFlow = MutableStateFlow(TopFilter.AroundMe), val defaultShortsFollowList: MutableStateFlow = 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) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt index f2a4369601..9cbfccfa28 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/RelaySubscriptionsCoordinator.kt @@ -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, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt index 1a0fdee837..68430b7104 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt @@ -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" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 980067c770..c08dcb5da7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -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 { AwardBadgeScreen(it.kind, it.pubKeyHex, it.dTag, accountViewModel, nav) } composableFromEnd { PicturesScreen(accountViewModel, nav) } composableFromEnd { WorkoutsScreen(accountViewModel, nav) } + composableFromEnd { GitRepositoriesScreen(accountViewModel, nav) } composableFromEnd { SoftwareAppsScreen(accountViewModel, nav) } composableFromEnd { NappletsScreen(accountViewModel, nav) } composableFromEnd { NsitesScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt index c5c04ac01b..aab1307013 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/NavBarItem.kt @@ -52,6 +52,7 @@ enum class NavBarItem { ARTICLES, PICTURES, WORKOUTS, + GIT_REPOSITORIES, SOFTWARE_APPS, NAPPLETS, NSITES, @@ -219,6 +220,13 @@ val NavBarCatalog: Map = 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.POLLS, NavBarItem.PRODUCTS, NavBarItem.WORKOUTS, + NavBarItem.GIT_REPOSITORIES, NavBarItem.LIVE_STREAMS, NavBarItem.NESTS, NavBarItem.COMMUNITIES, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 002bad4a1a..cf31c37708 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -85,6 +85,8 @@ sealed class Route { @Serializable object Workouts : Route() + @Serializable object GitRepositories : Route() + @Serializable object SoftwareApps : Route() @Serializable object Napplets : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/MemoryUsageChip.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/MemoryUsageChip.kt index 82b6e42ab1..85a205c84e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/MemoryUsageChip.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/topbars/MemoryUsageChip.kt @@ -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(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) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt index 6f6892fbde..b1fc54e0bf 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Git.kt @@ -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, + ) + } + } + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index c253fcd35d..50cdfd204e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -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) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt index e98d500c0d..8052f08ab3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BottomBarFeedPreloaders.kt @@ -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 diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/GitRepositoriesScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/GitRepositoriesScreen.kt new file mode 100644 index 0000000000..3b03b5ba4c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/GitRepositoriesScreen.kt @@ -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() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/GitRepositoriesTopBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/GitRepositoriesTopBar.kt new file mode 100644 index 0000000000..d28ff11f96 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/GitRepositoriesTopBar.kt @@ -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, + ) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt new file mode 100644 index 0000000000..abdd4b4335 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/dal/GitRepositoriesFeedFilter.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.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() { + 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 { + 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): Set = innerApplyFilter(newItems) + + fun buildFilterParams(account: Account): FilterByListParams = + FilterByListParams.create( + account.liveGitRepositoriesFollowLists.value, + account.hiddenUsers.flow.value, + ) + + private fun innerApplyFilter(collection: Collection): Set { + 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): List = items.sortedByDefaultFeedOrder() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilter.kt new file mode 100644 index 0000000000..bc343e103f --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilter.kt @@ -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 = + 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() + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilterAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilterAssembler.kt new file mode 100644 index 0000000000..2514e313b2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilterAssembler.kt @@ -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() { + val group = + listOf( + GitRepositoriesSubAssembler(client, ::allKeys), + ) + + override fun invalidateKeys() = invalidateFilters() + + override fun invalidateFilters() = group.forEach { it.invalidateFilters() } + + override fun destroy() = group.forEach { it.destroy() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilterAssemblerSubscription.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilterAssemblerSubscription.kt new file mode 100644 index 0000000000..d43df35a33 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesFilterAssemblerSubscription.kt @@ -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) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesSubAssembler.kt new file mode 100644 index 0000000000..6fb5fc19c4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/GitRepositoriesSubAssembler.kt @@ -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, +) : PerUserAndFollowListEoseManager(client, allKeys) { + override fun updateFilter( + key: GitRepositoriesQueryState, + since: SincePerRelayMap?, + ): List { + 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>() + + @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() } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByAllCommunities.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByAllCommunities.kt new file mode 100644 index 0000000000..67dbf674eb --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByAllCommunities.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.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, + since: Long? = null, +): List { + 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 { + 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?, + since: Long? = null, +): List { + 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 { + 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() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByAuthors.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByAuthors.kt new file mode 100644 index 0000000000..1c6f3c3e97 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByAuthors.kt @@ -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, + since: Long? = null, +): List { + 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 { + 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 { + 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() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByFollows.kt new file mode 100644 index 0000000000..ed93f7038b --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByFollows.kt @@ -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 { + 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() + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByGeohashes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByGeohashes.kt new file mode 100644 index 0000000000..0ec1f50383 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByGeohashes.kt @@ -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, + since: Long?, +): List { + 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 { + 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() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByHashtag.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByHashtag.kt new file mode 100644 index 0000000000..d9d7768bed --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesByHashtag.kt @@ -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, + since: Long? = null, +): List = + 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 { + 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() +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesGlobal.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesGlobal.kt new file mode 100644 index 0000000000..c02eb4588c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/gitRepositories/datasource/subassemblies/FilterGitRepositoriesGlobal.kt @@ -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 { + 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, + ), + ) + } +} diff --git a/amethyst/src/main/res/values-es-rES/strings.xml b/amethyst/src/main/res/values-es-rES/strings.xml index 6545c56819..892d6a5979 100644 --- a/amethyst/src/main/res/values-es-rES/strings.xml +++ b/amethyst/src/main/res/values-es-rES/strings.xml @@ -719,6 +719,7 @@ Este nApplet quiere pagar una factura Lightning de %1$d sats. Fuente: %1$s + v%1$s Descargar Acerca de Plataformas @@ -878,6 +879,7 @@ Música Listas de reproducción Episodios + Podcasts Ver episodios Aún no se han encontrado episodios @@ -915,6 +917,7 @@ Añadir hashtag Añadir un hashtag 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. + Hashtag Añadir añadido por Notas fijadas @@ -941,6 +944,7 @@ Mis intereses Añadir hashtag Privado + %1$d hashtag(s) Acciones del conjunto de intereses Renombrar Clonar @@ -980,6 +984,7 @@ 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. Enviar en cadena en su lugar Recargar mint + %1$s sats Importe de recarga Mint a recargar Fondos de @@ -1297,6 +1302,7 @@ 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. Preferencias de filtrado Contenido bloqueado + Aún no has bloqueado a ningún usuario. Ninguna cuenta ha sido marcada como spam en esta sesión. No hay palabras ocultas. Añade una palabra a continuación para ocultar las publicaciones que la contengan. @@ -1355,6 +1361,7 @@ Suscripciones COUNT (%1$d) Eventos de salida (%1$d) %1$d autores + %1$d ids desde %1$s hasta %1$s límite %1$d @@ -1365,6 +1372,7 @@ Acepta hasta %1$s en el futuro hace %1$s + %1$s bits Retención de eventos Tamaño de contenido Conectividad @@ -1378,6 +1386,7 @@ Límite máximo (devolución de eventos) Longitud máxima de subID Token de Cashu + Mint: %1$s Canjear Copiar token Abrir en otra app @@ -1735,6 +1744,7 @@ Enviado Actualizar Todos + Zaps No-Zaps Añadir cartera Predeterminada @@ -1758,10 +1768,13 @@ Buscando tu monedero… 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. Saldo + Mints URL del mint Eliminar mint Añadir mint Historial + 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. + Guardando… Clave nutzap (avanzado) Una clave privada separada usada únicamente para recibir nutzaps NIP-61. No es tu clave de identidad Nostr. Generar una nueva clave @@ -1834,7 +1847,9 @@ ¿Pagar %1$s sat + hasta %2$s sat en comisiones? Verificar ✓ El mint es accesible + ✓ %1$s No se pudo alcanzar el mint: %1$s + Nutzap Nutzap fallido No hay clave pública de destinatario en la nota Los nutzaps son públicos y revelarían esta nota privada. Usa un zap Lightning en su lugar. @@ -1876,12 +1891,18 @@ nombre, NIP-05 o npub Cambiar destinatario Cantidad + sats 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. Comentario (opcional) Prioridad Lento + Normal Rápido ~1 h + ~30 min + ~10 min + %1$s sat/vB · %2$s + %1$s · %2$s sat/vB · %3$s Cargando estimaciones de comisión… Enviar %1$s sats @@ -1906,6 +1927,7 @@ Transacción Comisión Cambio + %1$s sats Hecho Error en: %1$s El pago se difundió (tx %1$s) pero el recibo no se publicó. @@ -1940,6 +1962,7 @@ Editar calendario Eventos en este calendario (%1$d) Aún no has creado ningún evento de calendario. + Tablón Mes Semana Día @@ -2001,6 +2024,7 @@ Asistiré Tal vez No puedo ir + Confirmaciones (%1$d) Sin RSVPs aún. Participantes (%1$d) En calendarios (%1$d) @@ -2010,6 +2034,7 @@ %1$s · termina %2$s Compartir evento de calendario Exportar al calendario (.ics) + recordatorios_calendario Recordatorios de calendario Aviso cuando un evento al que asistirás está a punto de comenzar. Evento de calendario @@ -2034,6 +2059,10 @@ Se enviará una notificación cuando un evento al que asistirás esté a punto de comenzar. Tiempo de antelación del recordatorio Cuántos minutos antes del evento quieres recibir la notificación. + + %1$d min + %1$d min + Compartir como enlace Nostr Compartir enlace de calendario Todos los calendarios @@ -2182,6 +2211,7 @@ Relés de entrada DM El usuario recibe mensajes directos en estos relés 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) + Paquetes de claves Relays de KeyPackage 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. Relés privados @@ -2226,17 +2256,27 @@ Bifurcación de Web: Clon: + Rama + Confirmar + Fusionar base + Se actualizó la solicitud de incorporación de cambios con una nueva confirmación. Abierto Fusionado Cerrado Borrador Resumen + Incidencias Parches y PRs + Abierto + Cerrado y resuelto + Sin título Acerca de Enlaces Mantenedores Temas Fork personal + nSite: %1$s + nApplet: %1$s Permisos: Apps y sitios Sitio raíz @@ -2310,12 +2350,15 @@ El evento no tiene suficiente información para crear un enlace magnético Mis listas Selecciona una lista para filtrar el tablón + Tablones + Hashtags Conjuntos de intereses Ubicaciones Comunidades Listas Algoritmos de feed Todos los algoritmos de feed favoritos + Relés Añadir algoritmo de feed a favoritos Eliminar de favoritos Algoritmos de feed favoritos @@ -2345,10 +2388,13 @@ Todo Personas Notas + Local + Relés Solo seguidos Más recientes Más antiguos Relevancia + Popular Filtros Restablecer Fuente @@ -2360,6 +2406,7 @@ Elige el idioma al que traducir el contenido. Preferencias de visualización de idioma Para cada par de idiomas traducidos, elige qué idioma mostrar primero. + %1$s → %2$s Buscar idiomas Añadir idioma Añadir par de idiomas @@ -2416,6 +2463,7 @@ Reintentando… Reintentar Descartar + %1$d/%2$d Difundiendo Difundiendo %1$s Difundiendo %1$d eventos… @@ -2512,10 +2560,13 @@ Packs de seguidos Republicaciones (16) Seguidos por geohash + GiftWraps Incidencia de Git Parche de Git Repositorio de Git Respuesta de Git + Solicitud de incorporación + Actualización de solicitud de incorporación Objetivos de Zap Seguidos por hashtag Destacados @@ -2527,20 +2578,27 @@ Marcadores con nombre Chats en directo Transmisiones en directo + Zaps Solicitud NWC Respuesta NWC Zaps privados Solicitud de Zap + Blogs Sala de reuniones Presencia en sala Espacio de reuniones Perfil Lista de silenciados + NNS + NIP + Nostr Connect Estado DVM Solicitud de contenido DVM Respuesta de contenido DVM Solicitud de usuario DVM Respuesta de usuario DVM + OTS + Pagar a Listas de personas Imágenes Entrenamientos @@ -2548,6 +2606,7 @@ Encuesta de Zap Encuesta Respuesta de encuesta + Mensajes directos NIP-04 Relés privados Relés proxy Mensaje público @@ -2565,6 +2624,7 @@ Estado del usuario Notas Ediciones + Torrents Comentarios de torrent Relés de confianza Proveedores de confianza diff --git a/amethyst/src/main/res/values-es-rMX/strings.xml b/amethyst/src/main/res/values-es-rMX/strings.xml index 4e42fcaa66..d7be104028 100644 --- a/amethyst/src/main/res/values-es-rMX/strings.xml +++ b/amethyst/src/main/res/values-es-rMX/strings.xml @@ -712,6 +712,7 @@ Este nApplet quiere pagar una factura Lightning de %1$d sats. Fuente: %1$s + v%1$s Descargar Acerca de Plataformas @@ -870,6 +871,7 @@ Música Listas de reproducción Episodios + Podcasts Ver episodios Aún no se han encontrado episodios @@ -907,6 +909,7 @@ Añadir hashtag Añadir un hashtag 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. + Hashtag Añadir añadido por Notas fijadas @@ -933,6 +936,7 @@ Mis intereses Añadir hashtag Privado + %1$d hashtag(s) Acciones del conjunto de intereses Renombrar Clonar @@ -972,6 +976,7 @@ 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. Enviar en cadena en su lugar Recargar mint + %1$s sats Importe de recarga Mint a recargar Fondos de @@ -1289,6 +1294,7 @@ 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. Preferencias de filtrado Contenido bloqueado + Aún no has bloqueado a ningún usuario. Ninguna cuenta ha sido marcada como spam en esta sesión. No hay palabras ocultas. Añade una palabra a continuación para ocultar las publicaciones que la contengan. @@ -1347,6 +1353,7 @@ Suscripciones COUNT (%1$d) Eventos de salida (%1$d) %1$d autores + %1$d ids desde %1$s hasta %1$s límite %1$d @@ -1357,6 +1364,7 @@ Acepta hasta %1$s en el futuro hace %1$s + %1$s bits Retención de eventos Tamaño de contenido Conectividad @@ -1370,6 +1378,7 @@ Límite máximo (devolución de eventos) Longitud máxima de subID Token de Cashu + Mint: %1$s Canjear Copiar token Abrir en otra app @@ -1727,6 +1736,7 @@ Enviado Actualizar Todos + Zaps No-Zaps Añadir cartera Predeterminada @@ -1750,10 +1760,13 @@ Buscando tu monedero… 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. Saldo + Mints URL del mint Eliminar mint Añadir mint Historial + 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. + Guardando… Clave nutzap (avanzado) Una clave privada separada usada únicamente para recibir nutzaps NIP-61. No es tu clave de identidad Nostr. Generar una nueva clave @@ -1826,7 +1839,9 @@ ¿Pagar %1$s sat + hasta %2$s sat en comisiones? Verificar ✓ El mint es accesible + ✓ %1$s No se pudo alcanzar el mint: %1$s + Nutzap Nutzap fallido No hay clave pública de destinatario en la nota Los nutzaps son públicos y revelarían esta nota privada. Usa un zap Lightning en su lugar. @@ -1868,12 +1883,18 @@ nombre, NIP-05 o npub Cambiar destinatario Cantidad + sats 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. Comentario (opcional) Prioridad Lento + Normal Rápido ~1 h + ~30 min + ~10 min + %1$s sat/vB · %2$s + %1$s · %2$s sat/vB · %3$s Cargando estimaciones de comisión… Enviar %1$s sats @@ -1898,6 +1919,7 @@ Transacción Comisión Cambio + %1$s sats Hecho Error en: %1$s El pago se difundió (tx %1$s) pero el recibo no se publicó. @@ -1932,6 +1954,7 @@ Editar calendario Eventos en este calendario (%1$d) Aún no has creado ningún evento de calendario. + Tablón Mes Semana Día @@ -1993,6 +2016,7 @@ Asistiré Tal vez No puedo ir + Confirmaciones (%1$d) Sin RSVPs aún. Participantes (%1$d) En calendarios (%1$d) @@ -2002,6 +2026,7 @@ %1$s · termina %2$s Compartir evento de calendario Exportar al calendario (.ics) + recordatorios_calendario Recordatorios de calendario Aviso cuando un evento al que asistirás está a punto de comenzar. Evento de calendario @@ -2026,6 +2051,10 @@ Se enviará una notificación cuando un evento al que asistirás esté a punto de comenzar. Tiempo de antelación del recordatorio Cuántos minutos antes del evento quieres recibir la notificación. + + %1$d min + %1$d min + Compartir como enlace Nostr Compartir enlace de calendario Todos los calendarios @@ -2174,6 +2203,7 @@ Relés de entrada DM El usuario recibe mensajes directos en estos relés 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) + Paquetes de claves Relays de KeyPackage 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. Relés privados @@ -2218,17 +2248,27 @@ Bifurcación de Web: Clon: + Rama + Confirmar + Fusionar base + Se actualizó la solicitud de incorporación de cambios con una nueva confirmación. Abierto Fusionado Cerrado Borrador Resumen + Incidencias Parches y PRs + Abierto + Cerrado y resuelto + Sin título Acerca de Enlaces Mantenedores Temas Fork personal + nSite: %1$s + nApplet: %1$s Permisos: Apps y sitios Sitio raíz @@ -2302,12 +2342,15 @@ El evento no tiene suficiente información para crear un enlace magnético Mis listas Selecciona una lista para filtrar el feed + Tablones + Hashtags Conjuntos de intereses Ubicaciones Comunidades Listas Algoritmos de feed Todos los algoritmos de feed favoritos + Relés Añadir algoritmo de feed a favoritos Eliminar de favoritos Algoritmos de feed favoritos @@ -2337,10 +2380,13 @@ Todo Personas Notas + Local + Relés Solo seguidos Más recientes Más antiguos Relevancia + Popular Filtros Restablecer Fuente @@ -2352,6 +2398,7 @@ Elige el idioma al que traducir el contenido. Preferencias de visualización de idioma Para cada par de idiomas traducidos, elige qué idioma mostrar primero. + %1$s → %2$s Buscar idiomas Añadir idioma Añadir par de idiomas @@ -2408,6 +2455,7 @@ Reintentando… Reintentar Descartar + %1$d/%2$d Difundiendo Difundiendo %1$s Difundiendo %1$d eventos… @@ -2504,10 +2552,13 @@ Packs de seguidos Republicaciones (16) Seguidos por geohash + GiftWraps Incidencia de Git Parche de Git Repositorio de Git Respuesta de Git + Solicitud de incorporación + Actualización de solicitud de incorporación Objetivos de Zap Seguidos por hashtag Destacados @@ -2519,20 +2570,27 @@ Marcadores con nombre Chats en directo Transmisiones en directo + Zaps Solicitud NWC Respuesta NWC Zaps privados Solicitud de Zap + Blogs Sala de reuniones Presencia en sala Espacio de reuniones Perfil Lista de silenciados + NNS + NIP + Nostr Connect Estado DVM Solicitud de contenido DVM Respuesta de contenido DVM Solicitud de usuario DVM Respuesta de usuario DVM + OTS + Pagar a Listas de personas Imágenes Entrenamientos @@ -2540,6 +2598,7 @@ Encuesta de Zap Encuesta Respuesta de encuesta + Mensajes directos NIP-04 Relés privados Relés proxy Mensaje público @@ -2557,6 +2616,7 @@ Estado del usuario Notas Ediciones + Torrents Comentarios de torrent Relés de confianza Proveedores de confianza diff --git a/amethyst/src/main/res/values-es-rUS/strings.xml b/amethyst/src/main/res/values-es-rUS/strings.xml index 23c75751b9..7d8cba22fa 100644 --- a/amethyst/src/main/res/values-es-rUS/strings.xml +++ b/amethyst/src/main/res/values-es-rUS/strings.xml @@ -712,6 +712,7 @@ Este nApplet quiere pagar una factura Lightning de %1$d sats. Fuente: %1$s + v%1$s Descargar Acerca de Plataformas @@ -870,6 +871,7 @@ Música Listas de reproducción Episodios + Podcasts Ver episodios Aún no se han encontrado episodios @@ -907,6 +909,7 @@ Añadir hashtag Añadir un hashtag 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. + Hashtag Añadir añadido por Notas fijadas @@ -933,6 +936,7 @@ Mis intereses Añadir hashtag Privado + %1$d hashtag(s) Acciones del conjunto de intereses Renombrar Clonar @@ -972,6 +976,7 @@ 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. Enviar en cadena en su lugar Recargar mint + %1$s sats Importe de recarga Mint a recargar Fondos de @@ -1289,6 +1294,7 @@ 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. Preferencias de filtrado Contenido bloqueado + Aún no has bloqueado a ningún usuario. Ninguna cuenta ha sido marcada como spam en esta sesión. No hay palabras ocultas. Añade una palabra a continuación para ocultar las publicaciones que la contengan. @@ -1347,6 +1353,7 @@ Suscripciones COUNT (%1$d) Eventos de salida (%1$d) %1$d autores + %1$d ids desde %1$s hasta %1$s límite %1$d @@ -1357,6 +1364,7 @@ Acepta hasta %1$s en el futuro hace %1$s + %1$s bits Retención de eventos Tamaño de contenido Conectividad @@ -1370,6 +1378,7 @@ Límite máximo (devolución de eventos) Longitud máxima de subID Token de Cashu + Mint: %1$s Canjear Copiar token Abrir en otra app @@ -1727,6 +1736,7 @@ Enviado Actualizar Todos + Zaps No-Zaps Añadir cartera Predeterminada @@ -1750,10 +1760,13 @@ Buscando tu monedero… 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. Saldo + Mints URL del mint Eliminar mint Añadir mint Historial + 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. + Guardando… Clave nutzap (avanzado) Una clave privada separada usada únicamente para recibir nutzaps NIP-61. No es tu clave de identidad Nostr. Generar una nueva clave @@ -1826,7 +1839,9 @@ ¿Pagar %1$s sat + hasta %2$s sat en comisiones? Verificar ✓ El mint es accesible + ✓ %1$s No se pudo alcanzar el mint: %1$s + Nutzap Nutzap fallido No hay clave pública de destinatario en la nota Los nutzaps son públicos y revelarían esta nota privada. Usa un zap Lightning en su lugar. @@ -1868,12 +1883,18 @@ nombre, NIP-05 o npub Cambiar destinatario Cantidad + sats 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. Comentario (opcional) Prioridad Lento + Normal Rápido ~1 h + ~30 min + ~10 min + %1$s sat/vB · %2$s + %1$s · %2$s sat/vB · %3$s Cargando estimaciones de comisión… Enviar %1$s sats @@ -1898,6 +1919,7 @@ Transacción Comisión Cambio + %1$s sats Hecho Error en: %1$s El pago se difundió (tx %1$s) pero el recibo no se publicó. @@ -1932,6 +1954,7 @@ Editar calendario Eventos en este calendario (%1$d) Aún no has creado ningún evento de calendario. + Tablón Mes Semana Día @@ -1993,6 +2016,7 @@ Asistiré Tal vez No puedo ir + Confirmaciones (%1$d) Sin RSVPs aún. Participantes (%1$d) En calendarios (%1$d) @@ -2002,6 +2026,7 @@ %1$s · termina %2$s Compartir evento de calendario Exportar al calendario (.ics) + recordatorios_calendario Recordatorios de calendario Aviso cuando un evento al que asistirás está a punto de comenzar. Evento de calendario @@ -2026,6 +2051,10 @@ Se enviará una notificación cuando un evento al que asistirás esté a punto de comenzar. Tiempo de antelación del recordatorio Cuántos minutos antes del evento quieres recibir la notificación. + + %1$d min + %1$d min + Compartir como enlace Nostr Compartir enlace de calendario Todos los calendarios @@ -2174,6 +2203,7 @@ Relés de entrada DM El usuario recibe mensajes directos en estos relés 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) + Paquetes de claves Relays de KeyPackage 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. Relés privados @@ -2218,17 +2248,27 @@ Bifurcación de Web: Clon: + Rama + Confirmar + Fusionar base + Se actualizó la solicitud de incorporación de cambios con una nueva confirmación. Abierto Fusionado Cerrado Borrador Resumen + Incidencias Parches y PRs + Abierto + Cerrado y resuelto + Sin título Acerca de Enlaces Mantenedores Temas Fork personal + nSite: %1$s + nApplet: %1$s Permisos: Apps y sitios Sitio raíz @@ -2302,12 +2342,15 @@ El evento no tiene suficiente información para crear un enlace magnético Mis listas Selecciona una lista para filtrar el feed + Tablones + Hashtags Conjuntos de intereses Ubicaciones Comunidades Listas Algoritmos de feed Todos los algoritmos de feed favoritos + Relés Añadir algoritmo de feed a favoritos Eliminar de favoritos Algoritmos de feed favoritos @@ -2337,10 +2380,13 @@ Todo Personas Notas + Local + Relés Solo seguidos Más recientes Más antiguos Relevancia + Popular Filtros Restablecer Fuente @@ -2352,6 +2398,7 @@ Elige el idioma al que traducir el contenido. Preferencias de visualización de idioma Para cada par de idiomas traducidos, elige qué idioma mostrar primero. + %1$s → %2$s Buscar idiomas Añadir idioma Añadir par de idiomas @@ -2408,6 +2455,7 @@ Reintentando… Reintentar Descartar + %1$d/%2$d Difundiendo Difundiendo %1$s Difundiendo %1$d eventos… @@ -2504,10 +2552,13 @@ Packs de seguidos Republicaciones (16) Seguidos por geohash + GiftWraps Incidencia de Git Parche de Git Repositorio de Git Respuesta de Git + Solicitud de incorporación + Actualización de solicitud de incorporación Objetivos de Zap Seguidos por hashtag Destacados @@ -2519,20 +2570,27 @@ Marcadores con nombre Chats en directo Transmisiones en directo + Zaps Solicitud NWC Respuesta NWC Zaps privados Solicitud de Zap + Blogs Sala de reuniones Presencia en sala Espacio de reuniones Perfil Lista de silenciados + NNS + NIP + Nostr Connect Estado DVM Solicitud de contenido DVM Respuesta de contenido DVM Solicitud de usuario DVM Respuesta de usuario DVM + OTS + Pagar a Listas de personas Imágenes Entrenamientos @@ -2540,6 +2598,7 @@ Encuesta de Zap Encuesta Respuesta de encuesta + Mensajes directos NIP-04 Relés privados Relés proxy Mensaje público @@ -2557,6 +2616,7 @@ Estado del usuario Notas Ediciones + Torrents Comentarios de torrent Relés de confianza Proveedores de confianza diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 0c3e73f40b..97e66fa674 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2580,6 +2580,7 @@ Maintainers Topics Personal fork + Git Repositories nSite: %1$s nApplet: %1$s Permissions: diff --git a/docs/changelog/translators.json b/docs/changelog/translators.json index 41f8a0df10..befde01fae 100644 --- a/docs/changelog/translators.json +++ b/docs/changelog/translators.json @@ -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": []