From 119e9b728144777fadc33dc4c8e024a0b173ea28 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 13 May 2024 15:42:52 +0200 Subject: [PATCH 01/28] Structure for Content Discovery --- .../ui/dal/DiscoverNIP89FeedFilter.kt | 84 +++++++++++++++++++ .../amethyst/ui/navigation/AppNavigation.kt | 3 + .../amethyst/ui/screen/FeedViewModel.kt | 12 +++ .../ui/screen/RememberForeverStates.kt | 1 + .../ui/screen/loggedIn/DiscoverScreen.kt | 10 +++ .../amethyst/ui/screen/loggedIn/MainScreen.kt | 8 ++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 119 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt new file mode 100644 index 000000000..397a1003c --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2024 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.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.events.AppDefinitionEvent +import com.vitorpamplona.quartz.events.MuteListEvent +import com.vitorpamplona.quartz.events.PeopleListEvent + +open class DiscoverNIP89FeedFilter( + val ktag: Int, + val account: Account, +) : AdditiveFeedFilter() { + override fun feedKey(): String { + return account.userProfile().pubkeyHex + "-" + followList() + } + + open fun followList(): String { + return account.defaultDiscoveryFollowList.value + } + + override fun showHiddenKey(): Boolean { + return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) + } + + override fun feed(): List { + val params = buildFilterParams(account) + + val notes = + LocalCache.notes.filterIntoSet { _, it -> + val noteEvent = it.event + noteEvent is AppDefinitionEvent // && params.match(noteEvent) + } + + return sort(notes) + } + + override fun applyFilter(collection: Set): Set { + return innerApplyFilter(collection) + } + + fun buildFilterParams(account: Account): FilterByListParams { + return FilterByListParams.create( + account.userProfile().pubkeyHex, + account.defaultDiscoveryFollowList.value, + account.liveDiscoveryFollowLists.value, + account.flowHiddenUsers.value, + ) + } + + protected open fun innerApplyFilter(collection: Collection): Set { + val params = buildFilterParams(account) + + return collection.filterTo(HashSet()) { + val noteEvent = it.event + noteEvent is AppDefinitionEvent // && params.match(noteEvent) + } + } + + override fun sort(collection: Set): List { + return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index bd13a00b0..ebbcc0ff5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel @@ -86,6 +87,7 @@ fun AppNavigation( newFeedViewModel: NostrChatroomListNewFeedViewModel, videoFeedViewModel: NostrVideoFeedViewModel, discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel, + discoverNip89FeedViewModel: NostrDiscoverNIP89FeedViewModel, discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel, discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel, discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel, @@ -173,6 +175,7 @@ fun AppNavigation( route.arguments, content = { DiscoverScreen( + discoveryContentNIP89FeedViewModel = discoverNip89FeedViewModel, discoveryMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel, discoveryLiveFeedViewModel = discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 7378ecf59..852705688 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -47,6 +47,7 @@ import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter +import com.vitorpamplona.amethyst.ui.dal.DiscoverNIP89FeedFilter import com.vitorpamplona.amethyst.ui.dal.DraftEventsFeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter @@ -109,6 +110,17 @@ class NostrDiscoverMarketplaceFeedViewModel(val account: Account) : } } +class NostrDiscoverNIP89FeedViewModel(val account: Account) : + FeedViewModel( + DiscoverNIP89FeedFilter(5003, account), + ) { + class Factory(val account: Account) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrDiscoverNIP89FeedViewModel { + return NostrDiscoverNIP89FeedViewModel(account) as NostrDiscoverNIP89FeedViewModel + } + } +} + class NostrDiscoverLiveFeedViewModel(val account: Account) : FeedViewModel(DiscoverLiveFeedFilter(account)) { class Factory(val account: Account) : ViewModelProvider.Factory { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt index b2d928fcc..e44a437ef 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt @@ -43,6 +43,7 @@ object ScrollStateKeys { val HOME_FOLLOWS = Route.Home.base + "Follows" val HOME_REPLIES = Route.Home.base + "FollowsReplies" + val DISCOVER_CONTENT = Route.Home.base + "Content" val DISCOVER_MARKETPLACE = Route.Home.base + "Marketplace" val DISCOVER_LIVE = Route.Home.base + "Live" val DISCOVER_COMMUNITY = Route.Home.base + "Communities" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index e6bf7d8d2..1d0050200 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -69,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys import com.vitorpamplona.amethyst.ui.screen.RefresheableBox import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState @@ -78,6 +79,7 @@ import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.TabRowHeight +import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent @@ -89,6 +91,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable fun DiscoverScreen( + discoveryContentNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel, discoveryMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel, discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel, discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel, @@ -106,6 +109,13 @@ fun DiscoverScreen( ) { mutableStateOf( listOf( + TabItem( + R.string.discover_content, + discoveryContentNIP89FeedViewModel, + Route.Discover.base + "Content", + ScrollStateKeys.DISCOVER_CONTENT, + AppDefinitionEvent.KIND, + ), TabItem( R.string.discover_marketplace, discoveryMarketplaceFeedViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index c04eb1a56..73f4d923d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -104,6 +104,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverNIP89FeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel @@ -207,6 +208,12 @@ fun MainScreen( factory = NostrDiscoverMarketplaceFeedViewModel.Factory(accountViewModel.account), ) + val discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel = + viewModel( + key = "NostrDiscoveryNIP89FeedViewModel", + factory = NostrDiscoverNIP89FeedViewModel.Factory(accountViewModel.account), + ) + val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel = viewModel( key = "NostrDiscoveryLiveFeedViewModel", @@ -411,6 +418,7 @@ fun MainScreen( knownFeedViewModel = knownFeedViewModel, newFeedViewModel = newFeedViewModel, videoFeedViewModel = videoFeedViewModel, + discoverNip89FeedViewModel = discoverNIP89FeedViewModel, discoverMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel, discoveryLiveFeedViewModel = discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82fb9fb56..14908ddb3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -490,6 +490,7 @@ Relays + Note Discovery Marketplace Live Community From 357981f2663493fc9896a8621cbc1c3d8acaae76 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 13 May 2024 16:12:19 +0200 Subject: [PATCH 02/28] add nip89 filter --- .../service/NostrDiscoveryDataSource.kt | 21 +++++++++++++++++++ .../ui/dal/DiscoverNIP89FeedFilter.kt | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index ba9f22348..91c572357 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter +import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent @@ -131,6 +132,25 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { ) } + fun createNIP89Filter(kTags: List): List { + return listOfNotNull( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + JsonFilter( + kinds = listOf(AppDefinitionEvent.KIND), + limit = 300, + tags = mapOf("k" to kTags), + since = + latestEOSEs.users[account.userProfile()] + ?.followList + ?.get(account.defaultDiscoveryFollowList.value) + ?.relayList, + ), + ), + ) + } + fun createLiveStreamFilter(): List { val follows = account.liveDiscoveryFollowLists.value?.users?.toList() @@ -404,6 +424,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { override fun updateChannelFilters() { discoveryFeedChannel.typedFilters = createLiveStreamFilter() + .plus(createNIP89Filter(listOf("5300"))) .plus(createPublicChatFilter()) .plus(createMarketplaceFilter()) .plus( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt index 397a1003c..ce0ef9513 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt @@ -48,7 +48,7 @@ open class DiscoverNIP89FeedFilter( val params = buildFilterParams(account) val notes = - LocalCache.notes.filterIntoSet { _, it -> + LocalCache.addressables.filterIntoSet { _, it -> val noteEvent = it.event noteEvent is AppDefinitionEvent // && params.match(noteEvent) } From 334b94890084644a357a315a0c80ead046e75b02 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 13 May 2024 17:27:34 +0200 Subject: [PATCH 03/28] app meta data, show content dvms --- .../ui/dal/DiscoverNIP89FeedFilter.kt | 1 - .../amethyst/ui/note/ChannelCardCompose.kt | 103 ++++++++++++++++++ .../vitorpamplona/amethyst/ui/note/Loaders.kt | 22 ++++ .../amethyst/ui/note/types/AppDefinition.kt | 4 +- .../amethyst/ui/screen/FeedViewModel.kt | 2 +- .../quartz/events/AppDefinitionEvent.kt | 87 ++++++++++++++- 6 files changed, 213 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt index ce0ef9513..306372b6b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt @@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent open class DiscoverNIP89FeedFilter( - val ktag: Int, val account: Account, ) : AdditiveFeedFilter() { override fun feedKey(): String { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 85f8ab9ca..0176f33fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -89,6 +89,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent @@ -213,6 +214,9 @@ fun InnerChannelCardWithReactions( is ClassifiedsEvent -> { InnerCardBox(baseNote, accountViewModel, nav) } + is AppDefinitionEvent -> { + InnerCardRow(baseNote, accountViewModel, nav) + } } } @@ -268,6 +272,9 @@ private fun RenderNoteRow( is ChannelCreateEvent -> { RenderChannelThumb(baseNote, accountViewModel, nav) } + is AppDefinitionEvent -> { + RenderContentDVMThumb(baseNote, accountViewModel, nav) + } } } @@ -516,6 +523,14 @@ data class CommunityCard( val moderators: ImmutableList, ) +@Immutable +data class DVMCard( + val name: String, + val description: String?, + val cover: String?, + val moderators: ImmutableList, +) + @Composable fun RenderCommunitiesThumb( baseNote: Note, @@ -715,6 +730,94 @@ private fun LoadParticipants( inner(participantUsers) } +@Composable +fun RenderContentDVMThumb( + baseNote: Note, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = baseNote.event as? AppDefinitionEvent ?: return + + val card by + baseNote + .live() + .metadata + .map { + val noteEvent = it.note.event as? AppDefinitionEvent + + DVMCard( + name = noteEvent?.appMetaData()?.name ?: "", + description = noteEvent?.appMetaData()?.about ?: "", + cover = noteEvent?.appMetaData()?.image?.ifBlank { null }, + moderators = persistentListOf(), + ) + } + .distinctUntilChanged() + .observeAsState( + DVMCard( + name = noteEvent.appMetaData()?.name ?: "", + description = noteEvent.appMetaData()?.about ?: "", + cover = noteEvent.appMetaData()?.image?.ifBlank { null }, + moderators = persistentListOf(), + ), + ) + + LeftPictureLayout( + onImage = { + card.cover?.let { + Box(contentAlignment = BottomStart) { + AsyncImage( + model = it, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize().clip(QuoteBorder), + ) + } + } ?: run { DisplayAuthorBanner(baseNote) } + }, + onTitleRow = { + Text( + text = card.name, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + + Spacer(modifier = StdHorzSpacer) + LikeReaction( + baseNote = baseNote, + grayTint = MaterialTheme.colorScheme.onSurface, + accountViewModel = accountViewModel, + nav, + ) + Spacer(modifier = StdHorzSpacer) + ZapReaction( + baseNote = baseNote, + grayTint = MaterialTheme.colorScheme.onSurface, + accountViewModel = accountViewModel, + nav = nav, + ) + }, + onDescription = { + card.description?.let { + Spacer(modifier = StdVertSpacer) + Row { + Text( + text = it, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + fontSize = 14.sp, + ) + } + } + }, + onBottomRow = { + }, + ) +} + @Composable fun RenderChannelThumb( baseNote: Note, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index 3379d9025..f7bab1a9d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -234,3 +234,25 @@ fun LoadChannel( channel?.let { content(it) } } + +@Composable +fun LoadDVMNip89( + baseChannelHex: String, + accountViewModel: AccountViewModel, + content: @Composable (Channel) -> Unit, +) { + var channel by + remember(baseChannelHex) { + mutableStateOf(accountViewModel.getChannelIfExists(baseChannelHex)) + } + + if (channel == null) { + LaunchedEffect(key1 = baseChannelHex) { + accountViewModel.checkGetOrCreateChannel(baseChannelHex) { newChannel -> + launch(Dispatchers.Main) { channel = newChannel } + } + } + } + + channel?.let { content(it) } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt index 663bec70d..49d813789 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/AppDefinition.kt @@ -70,8 +70,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size16Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.AppDefinitionEvent +import com.vitorpamplona.quartz.events.AppMetadata import com.vitorpamplona.quartz.events.EmptyTagList -import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.toImmutableListOfLists import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -85,7 +85,7 @@ fun RenderAppDefinition( ) { val noteEvent = note.event as? AppDefinitionEvent ?: return - var metadata by remember { mutableStateOf(null) } + var metadata by remember { mutableStateOf(null) } LaunchedEffect(key1 = noteEvent) { withContext(Dispatchers.Default) { metadata = noteEvent.appMetaData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 852705688..58d8dec22 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -112,7 +112,7 @@ class NostrDiscoverMarketplaceFeedViewModel(val account: Account) : class NostrDiscoverNIP89FeedViewModel(val account: Account) : FeedViewModel( - DiscoverNIP89FeedFilter(5003, account), + DiscoverNIP89FeedFilter(account), ) { class Factory(val account: Account) : ViewModelProvider.Factory { override fun create(modelClass: Class): NostrDiscoverNIP89FeedViewModel { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt index 8374c518d..f0cf4a7c3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/AppDefinitionEvent.kt @@ -22,11 +22,94 @@ package com.vitorpamplona.quartz.events import android.util.Log import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import com.fasterxml.jackson.annotation.JsonProperty import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import java.io.ByteArrayInputStream +@Stable +class AppMetadata { + var name: String? = null + var username: String? = null + + @JsonProperty("display_name") + var displayName: String? = null + var picture: String? = null + + var banner: String? = null + var image: String? = null + var website: String? = null + var about: String? = null + var subscription: Boolean? = false + var cashuAccepted: Boolean? = false + var encryptionSupported: Boolean? = false + var personalized: Boolean? = false + var amount: String? = null + + var nip05: String? = null + var domain: String? = null + var lud06: String? = null + var lud16: String? = null + + var twitter: String? = null + + @Transient + var tags: ImmutableListOfLists? = null + + fun anyName(): String? { + return displayName ?: name ?: username + } + + fun anyNameStartsWith(prefix: String): Boolean { + return listOfNotNull(name, username, displayName, nip05, lud06, lud16).any { + it.contains(prefix, true) + } + } + + fun lnAddress(): String? { + return lud16 ?: lud06 + } + + fun bestName(): String? { + return displayName ?: name ?: username + } + + fun nip05(): String? { + return nip05 + } + + fun profilePicture(): String? { + return picture + } + + fun cleanBlankNames() { + if (picture?.isNotEmpty() == true) picture = picture?.trim() + if (nip05?.isNotEmpty() == true) nip05 = nip05?.trim() + + if (displayName?.isNotEmpty() == true) displayName = displayName?.trim() + if (name?.isNotEmpty() == true) name = name?.trim() + if (username?.isNotEmpty() == true) username = username?.trim() + if (lud06?.isNotEmpty() == true) lud06 = lud06?.trim() + if (lud16?.isNotEmpty() == true) lud16 = lud16?.trim() + + if (website?.isNotEmpty() == true) website = website?.trim() + if (domain?.isNotEmpty() == true) domain = domain?.trim() + + if (picture?.isBlank() == true) picture = null + if (nip05?.isBlank() == true) nip05 = null + if (displayName?.isBlank() == true) displayName = null + if (name?.isBlank() == true) name = null + if (username?.isBlank() == true) username = null + if (lud06?.isBlank() == true) lud06 = null + if (lud16?.isBlank() == true) lud16 = null + + if (website?.isBlank() == true) website = null + if (domain?.isBlank() == true) domain = null + } +} + @Immutable class AppDefinitionEvent( id: HexKey, @@ -36,7 +119,7 @@ class AppDefinitionEvent( content: String, sig: HexKey, ) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient private var cachedMetadata: UserMetadata? = null + @Transient private var cachedMetadata: AppMetadata? = null fun appMetaData() = if (cachedMetadata != null) { @@ -46,7 +129,7 @@ class AppDefinitionEvent( val newMetadata = mapper.readValue( ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)), - UserMetadata::class.java, + AppMetadata::class.java, ) cachedMetadata = newMetadata From fe45e188bd0a9e0b87067ca2411f602a05ff6359 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 13 May 2024 18:17:36 +0200 Subject: [PATCH 04/28] hide subscription dvms for now --- .../amethyst/ui/note/ChannelCardCompose.kt | 3 -- .../ui/screen/RememberForeverStates.kt | 2 +- .../ui/screen/loggedIn/DiscoverScreen.kt | 39 ++++++++++++------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index 0176f33fb..533f3e93b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -528,7 +528,6 @@ data class DVMCard( val name: String, val description: String?, val cover: String?, - val moderators: ImmutableList, ) @Composable @@ -749,7 +748,6 @@ fun RenderContentDVMThumb( name = noteEvent?.appMetaData()?.name ?: "", description = noteEvent?.appMetaData()?.about ?: "", cover = noteEvent?.appMetaData()?.image?.ifBlank { null }, - moderators = persistentListOf(), ) } .distinctUntilChanged() @@ -758,7 +756,6 @@ fun RenderContentDVMThumb( name = noteEvent.appMetaData()?.name ?: "", description = noteEvent.appMetaData()?.about ?: "", cover = noteEvent.appMetaData()?.image?.ifBlank { null }, - moderators = persistentListOf(), ), ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt index e44a437ef..d4c0f453e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/RememberForeverStates.kt @@ -43,7 +43,7 @@ object ScrollStateKeys { val HOME_FOLLOWS = Route.Home.base + "Follows" val HOME_REPLIES = Route.Home.base + "FollowsReplies" - val DISCOVER_CONTENT = Route.Home.base + "Content" + val DISCOVER_CONTENT = Route.Home.base + "DiscoverContent" val DISCOVER_MARKETPLACE = Route.Home.base + "Marketplace" val DISCOVER_LIVE = Route.Home.base + "Live" val DISCOVER_COMMUNITY = Route.Home.base + "Communities" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index 1d0050200..ddf348a40 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -112,7 +112,7 @@ fun DiscoverScreen( TabItem( R.string.discover_content, discoveryContentNIP89FeedViewModel, - Route.Discover.base + "Content", + Route.Discover.base + "DiscoverContent", ScrollStateKeys.DISCOVER_CONTENT, AppDefinitionEvent.KIND, ), @@ -152,6 +152,7 @@ fun DiscoverScreen( val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size } WatchAccountForDiscoveryScreen( + discoverNIP89FeedViewModel = discoveryContentNIP89FeedViewModel, discoverMarketplaceFeedViewModel = discoveryMarketplaceFeedViewModel, discoveryLiveFeedViewModel = discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel, @@ -320,6 +321,7 @@ private fun RenderDiscoverFeed( @Composable fun WatchAccountForDiscoveryScreen( + discoverNIP89FeedViewModel: NostrDiscoverNIP89FeedViewModel, discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel, discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel, discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel, @@ -330,6 +332,7 @@ fun WatchAccountForDiscoveryScreen( LaunchedEffect(accountViewModel, listState) { NostrDiscoveryDataSource.resetFilters() + discoverNIP89FeedViewModel.checkKeysInvalidateDataAndSendToTop() discoverMarketplaceFeedViewModel.checkKeysInvalidateDataAndSendToTop() discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop() discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop() @@ -354,20 +357,30 @@ private fun DiscoverFeedLoaded( itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> val defaultModifier = remember { Modifier.fillMaxWidth().animateItemPlacement() } - Row(defaultModifier) { - ChannelCardCompose( - baseNote = item, - routeForLastRead = routeForLastRead, - modifier = Modifier.fillMaxWidth(), - forceEventKind = forceEventKind, - accountViewModel = accountViewModel, - nav = nav, + // TODO For now we avoid subscription based DVMs, as we need logic for these first if a user is not subscribed already. + var avoid = false + if (item.event is AppDefinitionEvent) { + if ((item.event as AppDefinitionEvent).appMetaData()?.subscription == true) { + avoid = true + } + } + // TODO End + if (!avoid) { + Row(defaultModifier) { + ChannelCardCompose( + baseNote = item, + routeForLastRead = routeForLastRead, + modifier = Modifier.fillMaxWidth(), + forceEventKind = forceEventKind, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + HorizontalDivider( + thickness = DividerThickness, ) } - - HorizontalDivider( - thickness = DividerThickness, - ) } } } From 896d227fea631d409df6e6e2f83f15eae59f3950 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 13 May 2024 19:25:43 +0200 Subject: [PATCH 05/28] Update DiscoverScreen.kt --- .../vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt index ddf348a40..df2072c7b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DiscoverScreen.kt @@ -103,6 +103,7 @@ fun DiscoverScreen( val tabs by remember( + discoveryContentNIP89FeedViewModel, discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel, From 2b7ef79d21e83084471ef8a9f8bf14dbde676dfe Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 13 May 2024 21:57:33 +0200 Subject: [PATCH 06/28] send dvm id to render list --- .../ui/dal/NIP90ContentDiscoveryFilter.kt | 46 +++++++++ .../amethyst/ui/navigation/AppNavigation.kt | 16 ++++ .../amethyst/ui/navigation/AppTopBar.kt | 2 + .../amethyst/ui/navigation/Routes.kt | 8 ++ .../amethyst/ui/note/NoteCompose.kt | 19 ++-- .../amethyst/ui/screen/FeedViewModel.kt | 10 ++ .../loggedIn/NIP90ContentDiscoveryScreen.kt | 96 +++++++++++++++++++ 7 files changed, 190 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt new file mode 100644 index 000000000..25161069a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 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.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note + +class NIP90ContentDiscoveryFilter(val account: Account) : FeedFilter() { + override fun feedKey(): String { + return account.userProfile().latestBookmarkList?.id ?: "" + } + + // TODO + override fun feed(): List { + val bookmarks = account.userProfile().latestBookmarkList + + val notes = + bookmarks?.taggedEvents()?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList() + val addresses = + bookmarks?.taggedAddresses()?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList() + + return notes + .plus(addresses) + .toSet() + .sortedWith(DefaultFeedOrder) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index ebbcc0ff5..e16d97ba3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -68,6 +68,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.NIP90ContentDiscoveryScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen @@ -218,8 +219,23 @@ fun AppNavigation( composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, nav) }) composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, nav) }) + composable(Route.Drafts.route, content = { DraftListScreen(accountViewModel, nav) }) + Route.ContentDiscovery.let { route -> + composable( + route.route, + route.arguments, + content = { + NIP90ContentDiscoveryScreen( + DVMID = it.arguments?.getString("id"), + accountViewModel = accountViewModel, + nav = nav, + ) + }, + ) + } + Route.Profile.let { route -> composable( route.route, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 32974b149..268d043f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -188,6 +188,8 @@ private fun RenderTopRouteBar( Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack) Route.Bookmarks.base -> TopBarWithBackButton(stringResource(id = R.string.bookmarks), navPopBack) Route.Drafts.base -> TopBarWithBackButton(stringResource(id = R.string.drafts), navPopBack) + Route.ContentDiscovery.base -> TopBarWithBackButton(stringResource(id = R.string.discover_content), navPopBack) + else -> { if (id != null) { when (currentRoute) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 0d5ccb18e..b9d0089c8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -148,6 +148,14 @@ sealed class Route( contentDescriptor = R.string.route_home, ) + object ContentDiscovery : + Route( + icon = R.drawable.ic_bookmarks, + contentDescriptor = R.string.discover_content, + route = "ContentDiscovery/{id}", + arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(), + ) + object Drafts : Route( route = "Drafts", diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 5a8ea4785..8b0d0dc87 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -398,13 +398,18 @@ fun ClickableNote( .combinedClickable( onClick = { scope.launch { - val redirectToNote = - if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { - baseNote.replyTo?.lastOrNull() ?: baseNote - } else { - baseNote - } - routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) } + if (baseNote.event is AppDefinitionEvent) { + // nav(Route.ContentDiscovery.route + "/${(baseNote.event as AppDefinitionEvent).pubKey()}") + nav("ContentDiscovery/${(baseNote.event as AppDefinitionEvent).pubKey()}") + } else { + val redirectToNote = + if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) { + baseNote.replyTo?.lastOrNull() ?: baseNote + } else { + baseNote + } + routeFor(redirectToNote, accountViewModel.userProfile())?.let { nav(it) } + } } }, onLongClick = showPopup, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 58d8dec22..a10a8a1a0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -281,6 +281,16 @@ class NostrBookmarkPrivateFeedViewModel(val account: Account) : } } +@Stable +class NostrNIP90ContentDiscoveryFeedViewModel(val account: Account) : + FeedViewModel(BookmarkPrivateFeedFilter(account)) { + class Factory(val account: Account) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrNIP90ContentDiscoveryFeedViewModel { + return NostrNIP90ContentDiscoveryFeedViewModel(account) as NostrNIP90ContentDiscoveryFeedViewModel + } + } +} + @Stable class NostrDraftEventsFeedViewModel(val account: Account) : FeedViewModel(DraftEventsFeedFilter(account)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt new file mode 100644 index 000000000..ee7fd853a --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2024 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 + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView + +@Composable +fun NIP90ContentDiscoveryScreen( + DVMID: String?, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel = + viewModel( + key = "NostrNIP90ContentDiscoveryFeedViewModel", + factory = NostrNIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account), + ) + + val userState by accountViewModel.account.decryptBookmarks.observeAsState() + + LaunchedEffect(userState) { + resultFeedViewModel.invalidateData() + } + + RenderNostrNIP90ContentDiscoveryScreen(DVMID, accountViewModel, nav, resultFeedViewModel) +} + +@Composable +@OptIn(ExperimentalFoundationApi::class) +private fun RenderNostrNIP90ContentDiscoveryScreen( + DVMID: String?, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel, +) { + Column(Modifier.fillMaxHeight()) { + val pagerState = rememberPagerState { 2 } + val coroutineScope = rememberCoroutineScope() + + if (DVMID != null) { + Text(text = "Debug: DVM KEY:\n " + DVMID) + } + // TODO Send KIND 5300 Event with p tag = DVMID + + /*note.event?.let { + ReactionEvent.create(emojiUrl, it, signer) { + Client.send(it) + LocalCache.consume(it) + } + }*/ + + // TODO PARSE AND LOAD RESULTS FROM KIND 6300 REPLY to resultfeedmodel (RN this still is the bookmark list) + // TODO Render Results + + HorizontalPager(state = pagerState) { + RefresheableFeedView( + resultFeedViewModel, + null, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} From baaa984d0d0c366e78acce3fbd6affcfe098746f Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Mon, 13 May 2024 22:43:38 +0200 Subject: [PATCH 07/28] add nip90 content discovery request --- .../loggedIn/NIP90ContentDiscoveryScreen.kt | 36 ++++++----- .../NIP90ContentDiscoveryRequestEvent.kt | 60 +++++++++++++++++++ 2 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryRequestEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index ee7fd853a..b4ac148e7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -72,25 +72,31 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( if (DVMID != null) { Text(text = "Debug: DVM KEY:\n " + DVMID) } - // TODO Send KIND 5300 Event with p tag = DVMID - /*note.event?.let { - ReactionEvent.create(emojiUrl, it, signer) { - Client.send(it) - LocalCache.consume(it) + if (DVMID != null) { + // TODO 1 Send KIND 5300 Event with p tag = DVMID (crashes) + + /* + var signer = accountViewModel.account.signer + NIP90ContentDiscoveryRequestEvent.create(DVMID, signer) { + // Client.send(it) + // LocalCache.justConsume(it, null) } - }*/ - // TODO PARSE AND LOAD RESULTS FROM KIND 6300 REPLY to resultfeedmodel (RN this still is the bookmark list) - // TODO Render Results + */ - HorizontalPager(state = pagerState) { - RefresheableFeedView( - resultFeedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) + // TODO 2 PARSE AND LOAD RESULTS FROM KIND 6300 REPLY to resultfeedmodel (RN this still is the bookmark list) + + // TODO 3 Render Results (hopefully works when 2 is working) + + HorizontalPager(state = pagerState) { + RefresheableFeedView( + resultFeedViewModel, + null, + accountViewModel = accountViewModel, + nav = nav, + ) + } } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryRequestEvent.kt new file mode 100644 index 000000000..d8dbf27b8 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryRequestEvent.kt @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Stable +@Immutable +class NIP90ContentDiscoveryRequestEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + @Transient private var cachedMetadata: AppMetadata? = null + + companion object { + const val KIND = 5300 + + fun create( + addressedDVM: String, + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (NIP90ContentDiscoveryRequestEvent) -> Unit, + ) { + val content = "" + + // val clientTag = arrayOf("client", "Amethyst") + + val tags = mutableListOf>() + tags.add(arrayOf("p", addressedDVM)) + tags.add(arrayOf("alt", "NIP90 Content Discovery request")) + signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) + } + } +} From 52e79580bf11379205dd86f2a5aa09a14206ccd7 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 14 May 2024 17:03:13 +0200 Subject: [PATCH 08/28] attempt to add own filters and event kinds for NIP90 content related events --- .../amethyst/model/LocalCache.kt | 60 ++++++ .../service/NostrDiscoveryDataSource.kt | 22 +++ .../ui/dal/NIP90ContentDiscoveryFilter.kt | 62 ++++-- .../vitorpamplona/amethyst/ui/note/Loaders.kt | 22 --- .../amethyst/ui/note/NoteCompose.kt | 15 ++ .../types/NIP90ContentDiscoveryResponse.kt | 181 ++++++++++++++++++ .../amethyst/ui/screen/FeedViewModel.kt | 4 +- .../loggedIn/NIP90ContentDiscoveryScreen.kt | 16 +- .../NIP90ContentDiscoveryRequestEvent.kt | 8 +- .../NIP90ContentDiscoveryResponseEvent.kt | 53 +++++ .../quartz/events/NIP90StatusEvent.kt | 53 +++++ 11 files changed, 448 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryResponseEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90StatusEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 82346fcef..7f3b11e71 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -89,6 +89,8 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.MuteListEvent +import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.events.NNSEvent import com.vitorpamplona.quartz.events.OtsEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -384,6 +386,62 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: NIP90ContentDiscoveryResponseEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + val replyTo = computeReplyTo(event) + + note.loadEvent(event, author, replyTo) + + // Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} + // ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}") + + // Counts the replies + replyTo.forEach { it.addReply(note) } + + refreshObservers(note) + } + + fun consume( + event: NIP90ContentDiscoveryRequestEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + val replyTo = computeReplyTo(event) + + note.loadEvent(event, author, replyTo) + + // Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} + // ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}") + + // Counts the replies + replyTo.forEach { it.addReply(note) } + + refreshObservers(note) + } + fun consume( event: GitPatchEvent, relay: Relay? = null, @@ -2299,6 +2357,8 @@ object LocalCache { } } is LnZapRequestEvent -> consume(event) + is NIP90ContentDiscoveryResponseEvent -> consume(event, relay) + is NIP90ContentDiscoveryRequestEvent -> consume(event, relay) is LnZapPaymentRequestEvent -> consume(event) is LnZapPaymentResponseEvent -> consume(event) is LongTextNoteEvent -> consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index 91c572357..01018a695 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -35,6 +35,8 @@ import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent +import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.events.NIP90StatusEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -151,6 +153,24 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { ) } + fun createNIP90ResponseFilter(): List { + return listOfNotNull( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + JsonFilter( + kinds = listOf(NIP90ContentDiscoveryResponseEvent.KIND, NIP90StatusEvent.KIND), + limit = 300, + since = + latestEOSEs.users[account.userProfile()] + ?.followList + ?.get(account.defaultDiscoveryFollowList.value) + ?.relayList, + ), + ), + ) + } + fun createLiveStreamFilter(): List { val follows = account.liveDiscoveryFollowLists.value?.users?.toList() @@ -425,6 +445,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { discoveryFeedChannel.typedFilters = createLiveStreamFilter() .plus(createNIP89Filter(listOf("5300"))) + .plus(createNIP90ResponseFilter()) .plus(createPublicChatFilter()) .plus(createMarketplaceFilter()) .plus( @@ -438,6 +459,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { createPublicChatsGeohashesFilter(), ), ) + .toList() .ifEmpty { null } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index 25161069a..411e76d05 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -23,24 +23,64 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.events.MuteListEvent +import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.events.PeopleListEvent -class NIP90ContentDiscoveryFilter(val account: Account) : FeedFilter() { +open class NIP90ContentDiscoveryFilter( + val account: Account, +) : AdditiveFeedFilter() { override fun feedKey(): String { - return account.userProfile().latestBookmarkList?.id ?: "" + return account.userProfile().pubkeyHex + "-" + followList() + } + + open fun followList(): String { + return account.defaultDiscoveryFollowList.value + } + + override fun showHiddenKey(): Boolean { + return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) } - // TODO override fun feed(): List { - val bookmarks = account.userProfile().latestBookmarkList + val params = buildFilterParams(account) val notes = - bookmarks?.taggedEvents()?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList() - val addresses = - bookmarks?.taggedAddresses()?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList() + LocalCache.notes.filterIntoSet { _, it -> + val noteEvent = it.event + noteEvent is NIP90ContentDiscoveryResponseEvent // && params.match(noteEvent) + } - return notes - .plus(addresses) - .toSet() - .sortedWith(DefaultFeedOrder) + return sort(notes) + } + + override fun applyFilter(collection: Set): Set { + var result = innerApplyFilter(collection) + println("Test World") + println(result) + return result + } + + fun buildFilterParams(account: Account): FilterByListParams { + return FilterByListParams.create( + account.userProfile().pubkeyHex, + account.defaultDiscoveryFollowList.value, + account.liveDiscoveryFollowLists.value, + account.flowHiddenUsers.value, + ) + } + + protected open fun innerApplyFilter(collection: Collection): Set { + val params = buildFilterParams(account) + + return collection.filterTo(HashSet()) { + val noteEvent = it.event + noteEvent is NIP90ContentDiscoveryResponseEvent // && params.match(noteEvent) + } + } + + override fun sort(collection: Set): List { + return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt index f7bab1a9d..3379d9025 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Loaders.kt @@ -234,25 +234,3 @@ fun LoadChannel( channel?.let { content(it) } } - -@Composable -fun LoadDVMNip89( - baseChannelHex: String, - accountViewModel: AccountViewModel, - content: @Composable (Channel) -> Unit, -) { - var channel by - remember(baseChannelHex) { - mutableStateOf(accountViewModel.getChannelIfExists(baseChannelHex)) - } - - if (channel == null) { - LaunchedEffect(key1 = baseChannelHex) { - accountViewModel.checkGetOrCreateChannel(baseChannelHex) { newChannel -> - launch(Dispatchers.Main) { channel = newChannel } - } - } - } - - channel?.let { content(it) } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 8b0d0dc87..9171ed4fa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -96,6 +96,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent +import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval @@ -157,6 +158,7 @@ import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent +import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent @@ -646,6 +648,19 @@ private fun RenderNoteRow( nav, ) } + is NIP90ContentDiscoveryResponseEvent -> + RenderNIP90ContentDiscoveryResponse( + baseNote, + makeItShort, + canPreview, + quotesLeft, + unPackReply, + backgroundColor, + editState, + accountViewModel, + nav, + ) + is PollNoteEvent -> { RenderPoll( baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt new file mode 100644 index 000000000..dad8a5460 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -0,0 +1,181 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.types + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.ui.components.SensitivityWarning +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent +import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition +import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.events.BaseTextNoteEvent +import com.vitorpamplona.quartz.events.CommunityDefinitionEvent +import com.vitorpamplona.quartz.events.EmptyTagList +import com.vitorpamplona.quartz.events.TextNoteEvent +import com.vitorpamplona.quartz.events.toImmutableListOfLists +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun RenderNIP90ContentDiscoveryResponse( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + unPackReply: Boolean, + backgroundColor: MutableState, + editState: State>, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event + val modifier = remember(note) { Modifier.fillMaxWidth() } + + if (noteEvent != null) { + TranslatableRichTextViewer( + content = noteEvent.content(), + canPreview = canPreview && !makeItShort, + quotesLeft = quotesLeft, + modifier = modifier, + tags = noteEvent.tags().toImmutableListOfLists(), + backgroundColor = backgroundColor, + id = note.idHex, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + val showReply by + remember(note) { + derivedStateOf { + noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser()) + } + } + + if (showReply) { + val replyingDirectlyTo = + remember(note) { + if (noteEvent is BaseTextNoteEvent) { + val replyingTo = noteEvent.replyingToAddressOrEvent() + if (replyingTo != null) { + val newNote = accountViewModel.getNoteIfExists(replyingTo) + if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) { + newNote + } else { + note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } + } + } else { + note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } + } + } else { + note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } + } + } + if (replyingDirectlyTo != null && unPackReply) { + ReplyNoteComposition(replyingDirectlyTo, backgroundColor, accountViewModel, nav) + Spacer(modifier = StdVertSpacer) + } + } + + LoadDecryptedContent( + note, + accountViewModel, + ) { body -> + val eventContent by + remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + val newBody = + if (editState.value is GenericLoadable.Loaded) { + val state = + (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow + state?.value?.event?.content() ?: body + } else { + body + } + + if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) { + "### $subject\n$newBody" + } else { + newBody + } + } + } + + val isAuthorTheLoggedUser = + remember(note.event) { accountViewModel.isLoggedUser(note.author) } + + if (makeItShort && isAuthorTheLoggedUser) { + Text( + text = eventContent, + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } else { + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val modifier = remember(note) { Modifier.fillMaxWidth() } + val tags = + remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + quotesLeft = quotesLeft, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + id = note.idHex, + accountViewModel = accountViewModel, + nav = nav, + ) + } + + if (note.event?.hasHashtags() == true) { + val hashtags = + remember(note.event) { + note.event?.hashtags()?.toImmutableList() ?: persistentListOf() + } + DisplayUncitedHashtags(hashtags, eventContent, nav) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index a10a8a1a0..0169f92c3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter +import com.vitorpamplona.amethyst.ui.dal.NIP90ContentDiscoveryFilter import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter @@ -283,7 +284,8 @@ class NostrBookmarkPrivateFeedViewModel(val account: Account) : @Stable class NostrNIP90ContentDiscoveryFeedViewModel(val account: Account) : - FeedViewModel(BookmarkPrivateFeedFilter(account)) { + // FeedViewModel(BookmarkPrivateFeedFilter(account)) { + FeedViewModel(NIP90ContentDiscoveryFilter(account)) { class Factory(val account: Account) : ViewModelProvider.Factory { override fun create(modelClass: Class): NostrNIP90ContentDiscoveryFeedViewModel { return NostrNIP90ContentDiscoveryFeedViewModel(account) as NostrNIP90ContentDiscoveryFeedViewModel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index b4ac148e7..fa9e7721d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -74,18 +74,18 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( } if (DVMID != null) { - // TODO 1 Send KIND 5300 Event with p tag = DVMID (crashes) + // TODO 1 Send KIND 5300 Event with p tag = DVMID (crashes, because cant map to event) - /* - var signer = accountViewModel.account.signer + /*val signer: NostrSigner = NostrSignerInternal(accountViewModel.account.keyPair) + println(accountViewModel.account.keyPair.pubKey.toHexKey()) NIP90ContentDiscoveryRequestEvent.create(DVMID, signer) { - // Client.send(it) - // LocalCache.justConsume(it, null) - } + Client.send(it) + LocalCache.justConsume(it, null) + } */ - */ + // var keyPair = accountViewModel.account.keyPair - // TODO 2 PARSE AND LOAD RESULTS FROM KIND 6300 REPLY to resultfeedmodel (RN this still is the bookmark list) + // TODO 2 PARSE AND LOAD RESULTS FROM KIND 6300 REPLY to resultfeedmodel (RN this doesnt show events) // TODO 3 Render Results (hopefully works when 2 is working) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryRequestEvent.kt index d8dbf27b8..d205a743b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryRequestEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryRequestEvent.kt @@ -35,9 +35,7 @@ class NIP90ContentDiscoveryRequestEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) { - @Transient private var cachedMetadata: AppMetadata? = null - +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { companion object { const val KIND = 5300 @@ -48,12 +46,10 @@ class NIP90ContentDiscoveryRequestEvent( onReady: (NIP90ContentDiscoveryRequestEvent) -> Unit, ) { val content = "" - - // val clientTag = arrayOf("client", "Amethyst") - val tags = mutableListOf>() tags.add(arrayOf("p", addressedDVM)) tags.add(arrayOf("alt", "NIP90 Content Discovery request")) + tags.add(arrayOf("client", "Amethyst")) signer.sign(createdAt, KIND, tags.toTypedArray(), content, onReady) } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryResponseEvent.kt new file mode 100644 index 000000000..8f76eb125 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90ContentDiscoveryResponseEvent.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class NIP90ContentDiscoveryResponseEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 6300 + const val ALT = "NIP90 Content Discovery reply" + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AppRecommendationEvent) -> Unit, + ) { + val tags = + arrayOf( + arrayOf("alt", ALT), + ) + signer.sign(createdAt, KIND, tags, "", onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90StatusEvent.kt new file mode 100644 index 000000000..2445aa41d --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90StatusEvent.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class NIP90StatusEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 7000 + const val ALT = "NIP90 Content Discovery reply" + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AppRecommendationEvent) -> Unit, + ) { + val tags = + arrayOf( + arrayOf("alt", ALT), + ) + signer.sign(createdAt, KIND, tags, "", onReady) + } + } +} From 0245c907ffaa1ed33626b07d623d262d6aefc152 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 14 May 2024 18:51:38 +0200 Subject: [PATCH 09/28] add nip90 events to factory --- .../amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt | 2 -- .../ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt | 6 ++---- .../java/com/vitorpamplona/quartz/events/EventFactory.kt | 3 +++ 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index 411e76d05..54b64fdd2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -57,8 +57,6 @@ open class NIP90ContentDiscoveryFilter( override fun applyFilter(collection: Set): Set { var result = innerApplyFilter(collection) - println("Test World") - println(result) return result } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index fa9e7721d..3d45096aa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -76,12 +76,10 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( if (DVMID != null) { // TODO 1 Send KIND 5300 Event with p tag = DVMID (crashes, because cant map to event) - /*val signer: NostrSigner = NostrSignerInternal(accountViewModel.account.keyPair) - println(accountViewModel.account.keyPair.pubKey.toHexKey()) - NIP90ContentDiscoveryRequestEvent.create(DVMID, signer) { + /*NIP90ContentDiscoveryRequestEvent.create(DVMID, accountViewModel.account.signer) { Client.send(it) LocalCache.justConsume(it, null) - } */ + }*/ // var keyPair = accountViewModel.account.keyPair diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index ed5346c3d..9acbef10f 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -114,6 +114,9 @@ class EventFactory { MetadataEvent.KIND -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) MuteListEvent.KIND -> MuteListEvent(id, pubKey, createdAt, tags, content, sig) NNSEvent.KIND -> NNSEvent(id, pubKey, createdAt, tags, content, sig) + NIP90StatusEvent.KIND -> NIP90StatusEvent(id, pubKey, createdAt, tags, content, sig) + NIP90ContentDiscoveryRequestEvent.KIND -> NIP90ContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) + NIP90ContentDiscoveryResponseEvent.KIND -> NIP90ContentDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig) PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig) PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig) From 8a50b3938ddb7174dc15a954df0f46d29f28ab0c Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 14 May 2024 21:41:36 +0200 Subject: [PATCH 10/28] send request, get results (still needs parsing) --- .../ui/dal/NIP90ContentDiscoveryFilter.kt | 59 ++++++++++++-- .../amethyst/ui/navigation/AppNavigation.kt | 12 +-- .../types/NIP90ContentDiscoveryResponse.kt | 77 ++++++------------- .../amethyst/ui/screen/FeedViewModel.kt | 8 +- .../loggedIn/NIP90ContentDiscoveryScreen.kt | 48 +++++++----- 5 files changed, 116 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index 54b64fdd2..c5d613d1d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -20,15 +20,19 @@ */ package com.vitorpamplona.amethyst.ui.dal +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.events.PeopleListEvent open class NIP90ContentDiscoveryFilter( val account: Account, + val dvmkey: String, ) : AdditiveFeedFilter() { override fun feedKey(): String { return account.userProfile().pubkeyHex + "-" + followList() @@ -49,15 +53,28 @@ open class NIP90ContentDiscoveryFilter( val notes = LocalCache.notes.filterIntoSet { _, it -> val noteEvent = it.event - noteEvent is NIP90ContentDiscoveryResponseEvent // && params.match(noteEvent) + noteEvent is NIP90ContentDiscoveryResponseEvent && it.event?.pubKey() == dvmkey && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) } + var sorted = sort(notes) + var note = sorted.first() - return sort(notes) + var eventContent = note.event?.content() + + var collection: Set = setOf() + val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + var json = mapper.readValue(eventContent, Array::class.java) + for (element in json) { + // var test = mapper.readValue(element.toString(), Array::class.java) + // TODO. This is ugly. how to Kotlin? + var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() + collection + id + } + + return sort(collection) } override fun applyFilter(collection: Set): Set { - var result = innerApplyFilter(collection) - return result + return innerApplyFilter(collection) } fun buildFilterParams(account: Account): FilterByListParams { @@ -70,12 +87,38 @@ open class NIP90ContentDiscoveryFilter( } protected open fun innerApplyFilter(collection: Collection): Set { - val params = buildFilterParams(account) + // val params = buildFilterParams(account) - return collection.filterTo(HashSet()) { - val noteEvent = it.event - noteEvent is NIP90ContentDiscoveryResponseEvent // && params.match(noteEvent) + val notes = + collection.filterTo(HashSet()) { + val noteEvent = it.event + noteEvent is NIP90ContentDiscoveryResponseEvent && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) + } + + // TODO. We want to parse the content of the latest event to ids and get the nodes from these ids + + /* var sorted = sort(notes) + var note = sorted.first() + + var eventContent = note.event?.content() + + val collection: Set = setOf() + val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + var json = mapper.readValue(eventContent, Array::class.java) + for (element in json) { + // var test = mapper.readValue(element.toString(), Array::class.java) + // TODO. This is ugly. how to Kotlin? + var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() + + var note = LocalCache.getNoteIfExists(id) + if (note != null) { + collection + note + } } + + return collection + */ + return notes } override fun sort(collection: Set): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index e16d97ba3..51250e69d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -227,11 +227,13 @@ fun AppNavigation( route.route, route.arguments, content = { - NIP90ContentDiscoveryScreen( - DVMID = it.arguments?.getString("id"), - accountViewModel = accountViewModel, - nav = nav, - ) + it.arguments?.getString("id")?.let { it1 -> + NIP90ContentDiscoveryScreen( + DVMID = it1, + accountViewModel = accountViewModel, + nav = nav, + ) + } }, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt index dad8a5460..11cd5087b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90ContentDiscoveryResponse.kt @@ -22,8 +22,6 @@ package com.vitorpamplona.amethyst.ui.note.types import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State @@ -32,7 +30,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextOverflow import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.SensitivityWarning @@ -42,7 +39,6 @@ import com.vitorpamplona.amethyst.ui.note.ReplyNoteComposition import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer -import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.BaseTextNoteEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.EmptyTagList @@ -66,20 +62,6 @@ fun RenderNIP90ContentDiscoveryResponse( val noteEvent = note.event val modifier = remember(note) { Modifier.fillMaxWidth() } - if (noteEvent != null) { - TranslatableRichTextViewer( - content = noteEvent.content(), - canPreview = canPreview && !makeItShort, - quotesLeft = quotesLeft, - modifier = modifier, - tags = noteEvent.tags().toImmutableListOfLists(), - backgroundColor = backgroundColor, - id = note.idHex, - accountViewModel = accountViewModel, - nav = nav, - ) - } - val showReply by remember(note) { derivedStateOf { @@ -140,42 +122,33 @@ fun RenderNIP90ContentDiscoveryResponse( val isAuthorTheLoggedUser = remember(note.event) { accountViewModel.isLoggedUser(note.author) } - if (makeItShort && isAuthorTheLoggedUser) { - Text( - text = eventContent, - color = MaterialTheme.colorScheme.placeholderText, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } else { - SensitivityWarning( - note = note, + SensitivityWarning( + note = note, + accountViewModel = accountViewModel, + ) { + val modifier = remember(note) { Modifier.fillMaxWidth() } + val tags = + remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + + TranslatableRichTextViewer( + content = eventContent, + canPreview = canPreview && !makeItShort, + quotesLeft = quotesLeft, + modifier = modifier, + tags = tags, + backgroundColor = backgroundColor, + id = note.idHex, accountViewModel = accountViewModel, - ) { - val modifier = remember(note) { Modifier.fillMaxWidth() } - val tags = - remember(note) { note.event?.tags()?.toImmutableListOfLists() ?: EmptyTagList } + nav = nav, + ) + } - TranslatableRichTextViewer( - content = eventContent, - canPreview = canPreview && !makeItShort, - quotesLeft = quotesLeft, - modifier = modifier, - tags = tags, - backgroundColor = backgroundColor, - id = note.idHex, - accountViewModel = accountViewModel, - nav = nav, - ) - } - - if (note.event?.hasHashtags() == true) { - val hashtags = - remember(note.event) { - note.event?.hashtags()?.toImmutableList() ?: persistentListOf() - } - DisplayUncitedHashtags(hashtags, eventContent, nav) - } + if (note.event?.hasHashtags() == true) { + val hashtags = + remember(note.event) { + note.event?.hashtags()?.toImmutableList() ?: persistentListOf() + } + DisplayUncitedHashtags(hashtags, eventContent, nav) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 0169f92c3..b1ab4d25b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -283,12 +283,12 @@ class NostrBookmarkPrivateFeedViewModel(val account: Account) : } @Stable -class NostrNIP90ContentDiscoveryFeedViewModel(val account: Account) : +class NostrNIP90ContentDiscoveryFeedViewModel(val account: Account, val dvmkey: String) : // FeedViewModel(BookmarkPrivateFeedFilter(account)) { - FeedViewModel(NIP90ContentDiscoveryFilter(account)) { - class Factory(val account: Account) : ViewModelProvider.Factory { + FeedViewModel(NIP90ContentDiscoveryFilter(account, dvmkey)) { + class Factory(val account: Account, val dvmkey: String) : ViewModelProvider.Factory { override fun create(modelClass: Class): NostrNIP90ContentDiscoveryFeedViewModel { - return NostrNIP90ContentDiscoveryFeedViewModel(account) as NostrNIP90ContentDiscoveryFeedViewModel + return NostrNIP90ContentDiscoveryFeedViewModel(account, dvmkey) as NostrNIP90ContentDiscoveryFeedViewModel } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index 3d45096aa..f061298a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -33,22 +33,25 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent @Composable fun NIP90ContentDiscoveryScreen( - DVMID: String?, + DVMID: String, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { val resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel = viewModel( key = "NostrNIP90ContentDiscoveryFeedViewModel", - factory = NostrNIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account), + factory = NostrNIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmkey = DVMID), ) - val userState by accountViewModel.account.decryptBookmarks.observeAsState() + val userState by accountViewModel.account.decryptBookmarks.observeAsState() // TODO LaunchedEffect(userState) { resultFeedViewModel.invalidateData() @@ -75,26 +78,33 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( if (DVMID != null) { // TODO 1 Send KIND 5300 Event with p tag = DVMID (crashes, because cant map to event) + val thread = + Thread { + try { + NIP90ContentDiscoveryRequestEvent.create(DVMID, accountViewModel.account.signer) { + Client.send(it) + LocalCache.justConsume(it, null) + } + } catch (e: Exception) { + e.printStackTrace() + } + } - /*NIP90ContentDiscoveryRequestEvent.create(DVMID, accountViewModel.account.signer) { - Client.send(it) - LocalCache.justConsume(it, null) - }*/ + thread.start() + } + // var keyPair = accountViewModel.account.keyPair - // var keyPair = accountViewModel.account.keyPair + // TODO 2 PARSE AND LOAD RESULTS FROM KIND 6300 REPLY to resultfeedmodel (RN this doesnt show events) - // TODO 2 PARSE AND LOAD RESULTS FROM KIND 6300 REPLY to resultfeedmodel (RN this doesnt show events) + // TODO 3 Render Results (hopefully works when 2 is working) - // TODO 3 Render Results (hopefully works when 2 is working) - - HorizontalPager(state = pagerState) { - RefresheableFeedView( - resultFeedViewModel, - null, - accountViewModel = accountViewModel, - nav = nav, - ) - } + HorizontalPager(state = pagerState) { + RefresheableFeedView( + resultFeedViewModel, + null, + accountViewModel = accountViewModel, + nav = nav, + ) } } } From df44e172ab437ba42f8e89ca8f1fd7fd92f0ec23 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 14 May 2024 21:48:59 +0200 Subject: [PATCH 11/28] Update NIP90ContentDiscoveryFilter.kt --- .../ui/dal/NIP90ContentDiscoveryFilter.kt | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index c5d613d1d..e0c42b4f5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -56,21 +56,25 @@ open class NIP90ContentDiscoveryFilter( noteEvent is NIP90ContentDiscoveryResponseEvent && it.event?.pubKey() == dvmkey && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) } var sorted = sort(notes) - var note = sorted.first() + if (sorted.isNotEmpty()) { + var note = sorted.first() - var eventContent = note.event?.content() + var eventContent = note.event?.content() - var collection: Set = setOf() - val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - var json = mapper.readValue(eventContent, Array::class.java) - for (element in json) { - // var test = mapper.readValue(element.toString(), Array::class.java) - // TODO. This is ugly. how to Kotlin? - var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() - collection + id + var collection: Set = setOf() + val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + var json = mapper.readValue(eventContent, Array::class.java) + for (element in json) { + // var test = mapper.readValue(element.toString(), Array::class.java) + // TODO. This is ugly. how to Kotlin? + var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() + collection + id + } + + return sort(collection) + } else { + return sort(notes) } - - return sort(collection) } override fun applyFilter(collection: Set): Set { @@ -98,25 +102,29 @@ open class NIP90ContentDiscoveryFilter( // TODO. We want to parse the content of the latest event to ids and get the nodes from these ids /* var sorted = sort(notes) - var note = sorted.first() + if (sorted.isNotEmpty()) { + var note = sorted.first() - var eventContent = note.event?.content() + var eventContent = note.event?.content() - val collection: Set = setOf() - val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - var json = mapper.readValue(eventContent, Array::class.java) - for (element in json) { - // var test = mapper.readValue(element.toString(), Array::class.java) - // TODO. This is ugly. how to Kotlin? - var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() + val collection: Set = setOf() + val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + var json = mapper.readValue(eventContent, Array::class.java) + for (element in json) { + // var test = mapper.readValue(element.toString(), Array::class.java) + // TODO. This is ugly. how to Kotlin? + var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() - var note = LocalCache.getNoteIfExists(id) - if (note != null) { - collection + note + var note = LocalCache.getNoteIfExists(id) + if (note != null) { + collection + note + } } + return collection + } else { + return notes } - return collection */ return notes } From ea8affaebfb9f6b993b2d773020f9ec23449bfe8 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 14 May 2024 22:01:03 +0200 Subject: [PATCH 12/28] general (unpolished) workflow works --- .../ui/dal/NIP90ContentDiscoveryFilter.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index e0c42b4f5..e57924843 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -61,14 +61,17 @@ open class NIP90ContentDiscoveryFilter( var eventContent = note.event?.content() - var collection: Set = setOf() + var collection: HashSet = hashSetOf() val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) var json = mapper.readValue(eventContent, Array::class.java) for (element in json) { // var test = mapper.readValue(element.toString(), Array::class.java) // TODO. This is ugly. how to Kotlin? var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() - collection + id + var note = LocalCache.checkGetOrCreateNote(id) + if (note != null) { + collection.add(note) + } } return sort(collection) @@ -101,13 +104,13 @@ open class NIP90ContentDiscoveryFilter( // TODO. We want to parse the content of the latest event to ids and get the nodes from these ids - /* var sorted = sort(notes) + var sorted = sort(notes) if (sorted.isNotEmpty()) { var note = sorted.first() var eventContent = note.event?.content() - val collection: Set = setOf() + val collection: HashSet = hashSetOf() val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) var json = mapper.readValue(eventContent, Array::class.java) for (element in json) { @@ -115,17 +118,16 @@ open class NIP90ContentDiscoveryFilter( // TODO. This is ugly. how to Kotlin? var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() - var note = LocalCache.getNoteIfExists(id) + var note = LocalCache.checkGetOrCreateNote(id) if (note != null) { - collection + note + collection.add(note) } } return collection } else { - return notes + return notes } - */ return notes } From 13a53876d0b98866dfdefc2c69ce99d50443dbe7 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 14 May 2024 22:21:03 +0200 Subject: [PATCH 13/28] fix sorting --- .../amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index e57924843..8e60b3d26 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -74,7 +74,7 @@ open class NIP90ContentDiscoveryFilter( } } - return sort(collection) + return collection.toList() } else { return sort(notes) } @@ -132,6 +132,6 @@ open class NIP90ContentDiscoveryFilter( } override fun sort(collection: Set): List { - return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + return collection.toList() // collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() } } From 39f87af072cbcdda1da553f590041b4817259736 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Tue, 14 May 2024 23:09:43 +0200 Subject: [PATCH 14/28] remove notifications for NIP90 Content events --- .../amethyst/ui/dal/NotificationFeedFilter.kt | 4 +++ .../loggedIn/NIP90ContentDiscoveryScreen.kt | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index e97dd069d..613d806f2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -37,6 +37,9 @@ import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.MuteListEvent +import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent +import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.events.NIP90StatusEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.RepostEvent @@ -111,6 +114,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() it.event !is LnZapRequestEvent && it.event !is BadgeDefinitionEvent && it.event !is BadgeProfilesEvent && + it.event !is NIP90ContentDiscoveryResponseEvent && it.event !is NIP90StatusEvent && it.event !is NIP90ContentDiscoveryRequestEvent && it.event !is GiftWrapEvent && (it.event is LnZapEvent || notifAuthor != loggedInUserHex) && (filterParams.isGlobal || filterParams.followLists?.users?.contains(notifAuthor) == true) && diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index f061298a8..bbc676327 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -72,12 +71,24 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( val pagerState = rememberPagerState { 2 } val coroutineScope = rememberCoroutineScope() - if (DVMID != null) { - Text(text = "Debug: DVM KEY:\n " + DVMID) - } + // TODO Render a nice header with image and DVM name from the id + + /* if (DVMID != null) { + LoadNote(baseNoteHex = DVMID, accountViewModel = accountViewModel) { + if (it != null) { + NoteCompose(baseNote = it, quotesLeft = 0, accountViewModel = accountViewModel ) { + + } + } + if (it != null) { + Text(text = (it.event as AppDefinitionEvent).content()) + } else { + Text(text = "yo") + } + } + } */ if (DVMID != null) { - // TODO 1 Send KIND 5300 Event with p tag = DVMID (crashes, because cant map to event) val thread = Thread { try { @@ -92,11 +103,6 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( thread.start() } - // var keyPair = accountViewModel.account.keyPair - - // TODO 2 PARSE AND LOAD RESULTS FROM KIND 6300 REPLY to resultfeedmodel (RN this doesnt show events) - - // TODO 3 Render Results (hopefully works when 2 is working) HorizontalPager(state = pagerState) { RefresheableFeedView( From f09b00ff01887b1680e61bb8a874447176eb43c5 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Wed, 15 May 2024 00:14:15 +0200 Subject: [PATCH 15/28] adding 5301 events to be handled --- .../amethyst/model/LocalCache.kt | 60 +++++++++++++++++++ .../quartz/events/EventFactory.kt | 2 + .../events/NIP90UserDiscoveryRequestEvent.kt | 53 ++++++++++++++++ .../events/NIP90UserDiscoveryResponseEvent.kt | 53 ++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90UserDiscoveryRequestEvent.kt create mode 100644 quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90UserDiscoveryResponseEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 7f3b11e71..880dafacc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -91,6 +91,8 @@ import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.events.NIP90UserDiscoveryRequestEvent +import com.vitorpamplona.quartz.events.NIP90UserDiscoveryResponseEvent import com.vitorpamplona.quartz.events.NNSEvent import com.vitorpamplona.quartz.events.OtsEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -442,6 +444,62 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: NIP90UserDiscoveryResponseEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + val replyTo = computeReplyTo(event) + + note.loadEvent(event, author, replyTo) + + // Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} + // ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}") + + // Counts the replies + replyTo.forEach { it.addReply(note) } + + refreshObservers(note) + } + + fun consume( + event: NIP90UserDiscoveryRequestEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + val replyTo = computeReplyTo(event) + + note.loadEvent(event, author, replyTo) + + // Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} + // ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}") + + // Counts the replies + replyTo.forEach { it.addReply(note) } + + refreshObservers(note) + } + fun consume( event: GitPatchEvent, relay: Relay? = null, @@ -2359,6 +2417,8 @@ object LocalCache { is LnZapRequestEvent -> consume(event) is NIP90ContentDiscoveryResponseEvent -> consume(event, relay) is NIP90ContentDiscoveryRequestEvent -> consume(event, relay) + is NIP90UserDiscoveryResponseEvent -> consume(event, relay) + is NIP90UserDiscoveryRequestEvent -> consume(event, relay) is LnZapPaymentRequestEvent -> consume(event) is LnZapPaymentResponseEvent -> consume(event) is LongTextNoteEvent -> consume(event, relay) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index 9acbef10f..812cbb07c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -117,6 +117,8 @@ class EventFactory { NIP90StatusEvent.KIND -> NIP90StatusEvent(id, pubKey, createdAt, tags, content, sig) NIP90ContentDiscoveryRequestEvent.KIND -> NIP90ContentDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) NIP90ContentDiscoveryResponseEvent.KIND -> NIP90ContentDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) + NIP90UserDiscoveryRequestEvent.KIND -> NIP90UserDiscoveryRequestEvent(id, pubKey, createdAt, tags, content, sig) + NIP90UserDiscoveryResponseEvent.KIND -> NIP90UserDiscoveryResponseEvent(id, pubKey, createdAt, tags, content, sig) OtsEvent.KIND -> OtsEvent(id, pubKey, createdAt, tags, content, sig) PeopleListEvent.KIND -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig) PinListEvent.KIND -> PinListEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90UserDiscoveryRequestEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90UserDiscoveryRequestEvent.kt new file mode 100644 index 000000000..493cbf407 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90UserDiscoveryRequestEvent.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class NIP90UserDiscoveryRequestEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 5301 + const val ALT = "NIP90 Content Discovery request" + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AppRecommendationEvent) -> Unit, + ) { + val tags = + arrayOf( + arrayOf("alt", ALT), + ) + signer.sign(createdAt, KIND, tags, "", onReady) + } + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90UserDiscoveryResponseEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90UserDiscoveryResponseEvent.kt new file mode 100644 index 000000000..f1978ca97 --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90UserDiscoveryResponseEvent.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.TimeUtils + +@Immutable +class NIP90UserDiscoveryResponseEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: Array>, + content: String, + sig: HexKey, +) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { + companion object { + const val KIND = 6301 + const val ALT = "NIP90 Content Discovery reply" + + fun create( + signer: NostrSigner, + createdAt: Long = TimeUtils.now(), + onReady: (AppRecommendationEvent) -> Unit, + ) { + val tags = + arrayOf( + arrayOf("alt", ALT), + ) + signer.sign(createdAt, KIND, tags, "", onReady) + } + } +} From 142aca40ce13c0ac9f2abd05c4b1842aa1aa51f1 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Wed, 15 May 2024 12:06:12 +0200 Subject: [PATCH 16/28] add NIP90 status events, update NIP90 feeds based on request id --- .../amethyst/model/LocalCache.kt | 30 +++++ .../service/NostrDiscoveryDataSource.kt | 21 +++- .../ui/dal/DiscoverNIP89FeedFilter.kt | 8 +- .../ui/dal/NIP90ContentDiscoveryFilter.kt | 21 ++-- .../amethyst/ui/dal/NIP90StatusFilter.kt | 102 ++++++++++++++++ .../amethyst/ui/note/NoteCompose.kt | 15 +++ .../amethyst/ui/note/types/NIP90Status.kt | 111 ++++++++++++++++++ .../amethyst/ui/screen/FeedView.kt | 15 +++ .../amethyst/ui/screen/FeedViewModel.kt | 21 +++- .../loggedIn/NIP90ContentDiscoveryScreen.kt | 56 ++++++--- .../quartz/events/NIP90StatusEvent.kt | 2 +- 11 files changed, 366 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90StatusFilter.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 880dafacc..696ca2bcf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -91,6 +91,7 @@ import com.vitorpamplona.quartz.events.MetadataEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.events.NIP90StatusEvent import com.vitorpamplona.quartz.events.NIP90UserDiscoveryRequestEvent import com.vitorpamplona.quartz.events.NIP90UserDiscoveryResponseEvent import com.vitorpamplona.quartz.events.NNSEvent @@ -444,6 +445,34 @@ object LocalCache { refreshObservers(note) } + fun consume( + event: NIP90StatusEvent, + relay: Relay? = null, + ) { + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) + + if (relay != null) { + author.addRelayBeingUsed(relay, event.createdAt) + note.addRelay(relay) + } + + // Already processed this event. + if (note.event != null) return + + val replyTo = computeReplyTo(event) + + note.loadEvent(event, author, replyTo) + + // Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} + // ${note.event?.content()?.split("\n")?.take(100)} ${formattedDateTime(event.createdAt)}") + + // Counts the replies + replyTo.forEach { it.addReply(note) } + + refreshObservers(note) + } + fun consume( event: NIP90UserDiscoveryResponseEvent, relay: Relay? = null, @@ -2415,6 +2444,7 @@ object LocalCache { } } is LnZapRequestEvent -> consume(event) + is NIP90StatusEvent -> consume(event, relay) is NIP90ContentDiscoveryResponseEvent -> consume(event, relay) is NIP90ContentDiscoveryRequestEvent -> consume(event, relay) is NIP90UserDiscoveryResponseEvent -> consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt index 01018a695..ab0d301a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDiscoveryDataSource.kt @@ -159,7 +159,25 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { types = setOf(FeedType.GLOBAL), filter = JsonFilter( - kinds = listOf(NIP90ContentDiscoveryResponseEvent.KIND, NIP90StatusEvent.KIND), + kinds = listOf(NIP90ContentDiscoveryResponseEvent.KIND), + limit = 300, + since = + latestEOSEs.users[account.userProfile()] + ?.followList + ?.get(account.defaultDiscoveryFollowList.value) + ?.relayList, + ), + ), + ) + } + + fun createNIP90StatusFilter(): List { + return listOfNotNull( + TypedFilter( + types = setOf(FeedType.GLOBAL), + filter = + JsonFilter( + kinds = listOf(NIP90StatusEvent.KIND), limit = 300, since = latestEOSEs.users[account.userProfile()] @@ -446,6 +464,7 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") { createLiveStreamFilter() .plus(createNIP89Filter(listOf("5300"))) .plus(createNIP90ResponseFilter()) + .plus(createNIP90StatusFilter()) .plus(createPublicChatFilter()) .plus(createMarketplaceFilter()) .plus( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt index 306372b6b..cf08abb1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverNIP89FeedFilter.kt @@ -26,10 +26,14 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent +import com.vitorpamplona.quartz.utils.TimeUtils open class DiscoverNIP89FeedFilter( val account: Account, ) : AdditiveFeedFilter() { + val lastAnnounced = 90 * 24 * 60 * 60 // 90 Days ago + // TODO better than announced would be last active, as this requires the DVM provider to regularly update the NIP89 announcement + override fun feedKey(): String { return account.userProfile().pubkeyHex + "-" + followList() } @@ -49,7 +53,7 @@ open class DiscoverNIP89FeedFilter( val notes = LocalCache.addressables.filterIntoSet { _, it -> val noteEvent = it.event - noteEvent is AppDefinitionEvent // && params.match(noteEvent) + noteEvent is AppDefinitionEvent && noteEvent.createdAt > TimeUtils.now() - lastAnnounced // && params.match(noteEvent) } return sort(notes) @@ -73,7 +77,7 @@ open class DiscoverNIP89FeedFilter( return collection.filterTo(HashSet()) { val noteEvent = it.event - noteEvent is AppDefinitionEvent // && params.match(noteEvent) + noteEvent is AppDefinitionEvent && noteEvent.createdAt > TimeUtils.now() - lastAnnounced // && params.match(noteEvent) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index 8e60b3d26..b4a483f18 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -25,7 +25,6 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -33,9 +32,10 @@ import com.vitorpamplona.quartz.events.PeopleListEvent open class NIP90ContentDiscoveryFilter( val account: Account, val dvmkey: String, + val request: String, ) : AdditiveFeedFilter() { override fun feedKey(): String { - return account.userProfile().pubkeyHex + "-" + followList() + return account.userProfile().pubkeyHex + "-" + request } open fun followList(): String { @@ -53,8 +53,10 @@ open class NIP90ContentDiscoveryFilter( val notes = LocalCache.notes.filterIntoSet { _, it -> val noteEvent = it.event - noteEvent is NIP90ContentDiscoveryResponseEvent && it.event?.pubKey() == dvmkey && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) + noteEvent is NIP90ContentDiscoveryResponseEvent && it.event!!.isTaggedEvent(request) + // it.event?.pubKey() == dvmkey && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) } + var sorted = sort(notes) if (sorted.isNotEmpty()) { var note = sorted.first() @@ -99,16 +101,17 @@ open class NIP90ContentDiscoveryFilter( val notes = collection.filterTo(HashSet()) { val noteEvent = it.event - noteEvent is NIP90ContentDiscoveryResponseEvent && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) + noteEvent is NIP90ContentDiscoveryResponseEvent && // && + it.event!!.isTaggedEvent(request) // && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) } - // TODO. We want to parse the content of the latest event to ids and get the nodes from these ids - - var sorted = sort(notes) + val sorted = sort(notes) + println("REQUEST: " + request) if (sorted.isNotEmpty()) { var note = sorted.first() var eventContent = note.event?.content() + println(eventContent) val collection: HashSet = hashSetOf() val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) @@ -118,7 +121,7 @@ open class NIP90ContentDiscoveryFilter( // TODO. This is ugly. how to Kotlin? var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() - var note = LocalCache.checkGetOrCreateNote(id) + val note = LocalCache.checkGetOrCreateNote(id) if (note != null) { collection.add(note) } @@ -127,8 +130,6 @@ open class NIP90ContentDiscoveryFilter( } else { return notes } - - return notes } override fun sort(collection: Set): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90StatusFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90StatusFilter.kt new file mode 100644 index 000000000..48330e47f --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90StatusFilter.kt @@ -0,0 +1,102 @@ +/** + * Copyright (c) 2024 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.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.events.MuteListEvent +import com.vitorpamplona.quartz.events.NIP90StatusEvent +import com.vitorpamplona.quartz.events.PeopleListEvent + +open class NIP90StatusFilter( + val account: Account, + val dvmkey: String, + val request: String, +) : AdditiveFeedFilter() { + override fun feedKey(): String { + return account.userProfile().pubkeyHex + "-" + followList() + } + + open fun followList(): String { + return account.defaultDiscoveryFollowList.value + } + + override fun showHiddenKey(): Boolean { + return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) + } + + override fun feed(): List { + val params = buildFilterParams(account) + + val status = + LocalCache.notes.filterIntoSet { _, it -> + val noteEvent = it.event + noteEvent is NIP90StatusEvent && it.event?.pubKey() == dvmkey && + it.event!!.isTaggedEvent(request) + // && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) + } + if (status.isNotEmpty()) { + println("Found status") + return listOf(status.first()) + } else { + println("Empty status") + return listOf() + } + } + + override fun applyFilter(collection: Set): Set { + return innerApplyFilter(collection) + } + + fun buildFilterParams(account: Account): FilterByListParams { + return FilterByListParams.create( + account.userProfile().pubkeyHex, + account.defaultDiscoveryFollowList.value, + account.liveDiscoveryFollowLists.value, + account.flowHiddenUsers.value, + ) + } + + protected open fun innerApplyFilter(collection: Collection): Set { + // val params = buildFilterParams(account) + + val status = + LocalCache.notes.filterIntoSet { _, it -> + val noteEvent = it.event + noteEvent is NIP90StatusEvent && it.event?.pubKey() == dvmkey && + it.event!!.isTaggedEvent(request) + // && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) + } + if (status.isNotEmpty()) { + println("Found status") + return setOf(status.first()) + } else { + println("Empty status") + return setOf() + } + } + + override fun sort(collection: Set): List { + return collection.toList() // collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index ae128173b..472a69946 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -100,6 +100,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityEvent import com.vitorpamplona.amethyst.ui.note.types.RenderLongFormContent import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90ContentDiscoveryResponse +import com.vitorpamplona.amethyst.ui.note.types.RenderNIP90Status import com.vitorpamplona.amethyst.ui.note.types.RenderPinListEvent import com.vitorpamplona.amethyst.ui.note.types.RenderPoll import com.vitorpamplona.amethyst.ui.note.types.RenderPostApproval @@ -163,6 +164,7 @@ import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent +import com.vitorpamplona.quartz.events.NIP90StatusEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PinListEvent import com.vitorpamplona.quartz.events.PollNoteEvent @@ -683,6 +685,19 @@ private fun RenderNoteRow( nav, ) + is NIP90StatusEvent -> + RenderNIP90Status( + baseNote, + makeItShort, + canPreview, + quotesLeft, + unPackReply, + backgroundColor, + editState, + accountViewModel, + nav, + ) + is PollNoteEvent -> { RenderPoll( baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt new file mode 100644 index 000000000..0c3662332 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/types/NIP90Status.kt @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.note.types + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.components.GenericLoadable +import com.vitorpamplona.amethyst.ui.note.LoadDecryptedContent +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.quartz.events.BaseTextNoteEvent +import com.vitorpamplona.quartz.events.CommunityDefinitionEvent +import com.vitorpamplona.quartz.events.TextNoteEvent + +@Composable +fun RenderNIP90Status( + note: Note, + makeItShort: Boolean, + canPreview: Boolean, + quotesLeft: Int, + unPackReply: Boolean, + backgroundColor: MutableState, + editState: State>, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + val noteEvent = note.event + val modifier = remember(note) { Modifier.fillMaxWidth() } + + val showReply by + remember(note) { + derivedStateOf { + noteEvent is BaseTextNoteEvent && !makeItShort && unPackReply && (note.replyTo != null || noteEvent.hasAnyTaggedUser()) + } + } + + if (showReply) { + val replyingDirectlyTo = + remember(note) { + if (noteEvent is BaseTextNoteEvent) { + val replyingTo = noteEvent.replyingToAddressOrEvent() + if (replyingTo != null) { + val newNote = accountViewModel.getNoteIfExists(replyingTo) + if (newNote != null && newNote.channelHex() == null && newNote.event?.kind() != CommunityDefinitionEvent.KIND) { + newNote + } else { + note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } + } + } else { + note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } + } + } else { + note.replyTo?.lastOrNull { it.event?.kind() != CommunityDefinitionEvent.KIND } + } + } + } + + LoadDecryptedContent( + note, + accountViewModel, + ) { body -> + val eventContent by + remember(note.event) { + derivedStateOf { + val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null } + val newBody = + if (editState.value is GenericLoadable.Loaded) { + val state = + (editState.value as? GenericLoadable.Loaded)?.loaded?.modificationToShow + state?.value?.event?.content() ?: body + } else { + body + } + + if (!subject.isNullOrBlank() && !newBody.split("\n")[0].contains(subject)) { + "### $subject\n$newBody" + } else { + newBody + } + } + } + + Text(text = eventContent) + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index a71951120..7304285ad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -77,6 +77,21 @@ fun RefresheableFeedView( } } +@Composable +fun DVMStatusView( + viewModel: FeedViewModel, + routeForLastRead: String?, + enablePullRefresh: Boolean = true, + scrollStateKey: String? = null, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, +) { + viewModel.invalidateData() + SaveableFeedState(viewModel, scrollStateKey) { listState -> + RenderFeedState(viewModel, accountViewModel, listState, nav, routeForLastRead) + } +} + @Composable fun RefresheableBox( viewModel: InvalidatableViewModel, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index b1ab4d25b..6bff6d6a3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -55,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.NIP90ContentDiscoveryFilter +import com.vitorpamplona.amethyst.ui.dal.NIP90StatusFilter import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter @@ -283,12 +284,22 @@ class NostrBookmarkPrivateFeedViewModel(val account: Account) : } @Stable -class NostrNIP90ContentDiscoveryFeedViewModel(val account: Account, val dvmkey: String) : - // FeedViewModel(BookmarkPrivateFeedFilter(account)) { - FeedViewModel(NIP90ContentDiscoveryFilter(account, dvmkey)) { - class Factory(val account: Account, val dvmkey: String) : ViewModelProvider.Factory { +class NostrNIP90ContentDiscoveryFeedViewModel(val account: Account, val dvmkey: String, val requestid: String) : + FeedViewModel(NIP90ContentDiscoveryFilter(account, dvmkey, requestid)) { + class Factory(val account: Account, val dvmkey: String, val requestid: String) : ViewModelProvider.Factory { override fun create(modelClass: Class): NostrNIP90ContentDiscoveryFeedViewModel { - return NostrNIP90ContentDiscoveryFeedViewModel(account, dvmkey) as NostrNIP90ContentDiscoveryFeedViewModel + println("FILTERREQUEST " + requestid) + return NostrNIP90ContentDiscoveryFeedViewModel(account, dvmkey, requestid) as NostrNIP90ContentDiscoveryFeedViewModel + } + } +} + +@Stable +class NostrNIP90StatusFeedViewModel(val account: Account, val dvmkey: String, val requestid: String) : + FeedViewModel(NIP90StatusFilter(account, dvmkey, requestid)) { + class Factory(val account: Account, val dvmkey: String, val requestid: String) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrNIP90StatusFeedViewModel { + return NostrNIP90StatusFeedViewModel(account, dvmkey, requestid) as NostrNIP90StatusFeedViewModel } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index bbc676327..53eefdae3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -34,7 +34,9 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relays.Client +import com.vitorpamplona.amethyst.ui.screen.DVMStatusView import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrNIP90StatusFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent @@ -44,10 +46,34 @@ fun NIP90ContentDiscoveryScreen( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { + var requestID = "" + val thread = + Thread { + try { + NIP90ContentDiscoveryRequestEvent.create(DVMID, accountViewModel.account.signer) { + Client.send(it) + requestID = it.id + println("REQUESTID: " + requestID) + LocalCache.justConsume(it, null) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + thread.start() + thread.join() + val resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel = viewModel( key = "NostrNIP90ContentDiscoveryFeedViewModel", - factory = NostrNIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmkey = DVMID), + factory = NostrNIP90ContentDiscoveryFeedViewModel.Factory(accountViewModel.account, dvmkey = DVMID, requestid = requestID), + ) + + val statusFeedViewModel: NostrNIP90StatusFeedViewModel = + viewModel( + key = "NostrNIP90StatusFeedViewModel", + factory = NostrNIP90StatusFeedViewModel.Factory(accountViewModel.account, dvmkey = DVMID, requestid = requestID), ) val userState by accountViewModel.account.decryptBookmarks.observeAsState() // TODO @@ -56,7 +82,7 @@ fun NIP90ContentDiscoveryScreen( resultFeedViewModel.invalidateData() } - RenderNostrNIP90ContentDiscoveryScreen(DVMID, accountViewModel, nav, resultFeedViewModel) + RenderNostrNIP90ContentDiscoveryScreen(DVMID, accountViewModel, nav, resultFeedViewModel, statusFeedViewModel) } @Composable @@ -66,11 +92,11 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( accountViewModel: AccountViewModel, nav: (String) -> Unit, resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel, + statusFeedViewModel: NostrNIP90StatusFeedViewModel, ) { Column(Modifier.fillMaxHeight()) { val pagerState = rememberPagerState { 2 } val coroutineScope = rememberCoroutineScope() - // TODO Render a nice header with image and DVM name from the id /* if (DVMID != null) { @@ -88,26 +114,22 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( } } */ - if (DVMID != null) { - val thread = - Thread { - try { - NIP90ContentDiscoveryRequestEvent.create(DVMID, accountViewModel.account.signer) { - Client.send(it) - LocalCache.justConsume(it, null) - } - } catch (e: Exception) { - e.printStackTrace() - } - } + // TODO only show this when the feed below hasnt loaded. I this possible? + // TODO render this more as a status label rather than a note - thread.start() - } + DVMStatusView( + statusFeedViewModel, + null, + enablePullRefresh = false, + accountViewModel = accountViewModel, + nav = nav, + ) HorizontalPager(state = pagerState) { RefresheableFeedView( resultFeedViewModel, null, + enablePullRefresh = false, accountViewModel = accountViewModel, nav = nav, ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90StatusEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90StatusEvent.kt index 2445aa41d..ad34650f1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90StatusEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP90StatusEvent.kt @@ -36,7 +36,7 @@ class NIP90StatusEvent( ) : Event(id, pubKey, createdAt, KIND, tags, content, sig) { companion object { const val KIND = 7000 - const val ALT = "NIP90 Content Discovery reply" + const val ALT = "NIP90 Status update" fun create( signer: NostrSigner, From 420323bcb0cd09fce309d5ceb23b1a0b9a2bda02 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Wed, 15 May 2024 15:11:36 +0200 Subject: [PATCH 17/28] adjustments and cleanups --- .../ui/dal/NIP90ContentDiscoveryFilter.kt | 1 - .../amethyst/ui/dal/NIP90StatusFilter.kt | 4 - .../amethyst/ui/screen/FeedView.kt | 16 ++++ .../amethyst/ui/screen/FeedViewModel.kt | 1 - .../loggedIn/NIP90ContentDiscoveryScreen.kt | 81 ++++++++++++------- 5 files changed, 69 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index b4a483f18..82398916b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -106,7 +106,6 @@ open class NIP90ContentDiscoveryFilter( } val sorted = sort(notes) - println("REQUEST: " + request) if (sorted.isNotEmpty()) { var note = sorted.first() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90StatusFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90StatusFilter.kt index 48330e47f..6cf787bfe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90StatusFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90StatusFilter.kt @@ -56,10 +56,8 @@ open class NIP90StatusFilter( // && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) } if (status.isNotEmpty()) { - println("Found status") return listOf(status.first()) } else { - println("Empty status") return listOf() } } @@ -88,10 +86,8 @@ open class NIP90StatusFilter( // && it.event?.isTaggedUser(account.keyPair.pubKey.toHexKey()) == true // && params.match(noteEvent) } if (status.isNotEmpty()) { - println("Found status") return setOf(status.first()) } else { - println("Empty status") return setOf() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 7304285ad..188fd589c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -301,3 +301,19 @@ fun FeedEmpty(onRefresh: () -> Unit) { OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) } } } + +@Composable +fun FeedEmptywithStatus( + status: String, + onRefresh: () -> Unit, +) { + Column( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text(status) + // Spacer(modifier = StdVertSpacer) + // OutlinedButton(onClick = onRefresh) { Text(text = stringResource(R.string.refresh)) } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 6bff6d6a3..891ec905a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -288,7 +288,6 @@ class NostrNIP90ContentDiscoveryFeedViewModel(val account: Account, val dvmkey: FeedViewModel(NIP90ContentDiscoveryFilter(account, dvmkey, requestid)) { class Factory(val account: Account, val dvmkey: String, val requestid: String) : ViewModelProvider.Factory { override fun create(modelClass: Class): NostrNIP90ContentDiscoveryFeedViewModel { - println("FILTERREQUEST " + requestid) return NostrNIP90ContentDiscoveryFeedViewModel(account, dvmkey, requestid) as NostrNIP90ContentDiscoveryFeedViewModel } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index 53eefdae3..83345640c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -34,10 +34,13 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relays.Client -import com.vitorpamplona.amethyst.ui.screen.DVMStatusView +import com.vitorpamplona.amethyst.ui.screen.FeedEmptywithStatus import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrNIP90StatusFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.RefresheableBox +import com.vitorpamplona.amethyst.ui.screen.RenderFeedState +import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState +import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent @Composable @@ -53,7 +56,6 @@ fun NIP90ContentDiscoveryScreen( NIP90ContentDiscoveryRequestEvent.create(DVMID, accountViewModel.account.signer) { Client.send(it) requestID = it.id - println("REQUESTID: " + requestID) LocalCache.justConsume(it, null) } } catch (e: Exception) { @@ -87,7 +89,7 @@ fun NIP90ContentDiscoveryScreen( @Composable @OptIn(ExperimentalFoundationApi::class) -private fun RenderNostrNIP90ContentDiscoveryScreen( +fun RenderNostrNIP90ContentDiscoveryScreen( DVMID: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit, @@ -97,42 +99,65 @@ private fun RenderNostrNIP90ContentDiscoveryScreen( Column(Modifier.fillMaxHeight()) { val pagerState = rememberPagerState { 2 } val coroutineScope = rememberCoroutineScope() - // TODO Render a nice header with image and DVM name from the id - - /* if (DVMID != null) { - LoadNote(baseNoteHex = DVMID, accountViewModel = accountViewModel) { - if (it != null) { - NoteCompose(baseNote = it, quotesLeft = 0, accountViewModel = accountViewModel ) { + // TODO 1 Render a nice header with image and DVM name from the id + // TODO How do we get the event information here? + var dvminfo = "DVM " + DVMID + if (DVMID != null) { + val thread = + Thread { + try { + var note = LocalCache.checkGetOrCreateNote(DVMID) + if (note != null) { + dvminfo = ((note.event as AppDefinitionEvent).appMetaData()?.name ?: "DVM from note") + } else { + dvminfo = "DVM from not found" + } + } catch (e: Exception) { + e.printStackTrace() } } - if (it != null) { - Text(text = (it.event as AppDefinitionEvent).content()) - } else { - Text(text = "yo") - } - } - } */ - // TODO only show this when the feed below hasnt loaded. I this possible? - // TODO render this more as a status label rather than a note + thread.start() + thread.join() + } - DVMStatusView( + // TODO 2 Get the latest event from the statusFeedViewModel + // TODO How do we extract the latest event.content (or event.status) from statusFeedViewModel + var dvmStatus = "DVM is processing..." + + /* if (statusFeedViewModel.localFilter.feed().isNotEmpty()) { + statusFeedViewModel.localFilter.feed()[0].event?.let { Text(text = it.content()) } + } else { + Text(text = "Nah") + } + + DVMStatusView( statusFeedViewModel, null, enablePullRefresh = false, accountViewModel = accountViewModel, nav = nav, - ) + )*/ + + // Text(text = dvminfo) HorizontalPager(state = pagerState) { - RefresheableFeedView( - resultFeedViewModel, - null, - enablePullRefresh = false, - accountViewModel = accountViewModel, - nav = nav, - ) + RefresheableBox(resultFeedViewModel, false) { + SaveableFeedState(resultFeedViewModel, null) { listState -> + RenderFeedState( + resultFeedViewModel, + accountViewModel, + listState, + nav, + null, + onEmpty = { + FeedEmptywithStatus(status = dvmStatus) { + } + }, + ) + } + } } } } From dc8209c90dc88b53338b6896e6dc427ec695c429 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Wed, 15 May 2024 15:13:16 +0200 Subject: [PATCH 18/28] Update NIP90ContentDiscoveryScreen.kt --- .../amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index 83345640c..66a40016f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -124,6 +124,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( // TODO 2 Get the latest event from the statusFeedViewModel // TODO How do we extract the latest event.content (or event.status) from statusFeedViewModel + // TODO We want to update dvmStatus with the content of the latest Status event var dvmStatus = "DVM is processing..." /* if (statusFeedViewModel.localFilter.feed().isNotEmpty()) { From 0b6cd08c4ab41be1a32b136e9f23c6163fe643e1 Mon Sep 17 00:00:00 2001 From: VASH Date: Wed, 15 May 2024 21:59:21 +0200 Subject: [PATCH 19/28] auto focus when entering the search screen --- .../ui/screen/loggedIn/SearchScreen.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 426131ee0..271ad4314 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -54,6 +54,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource @@ -91,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.findHashtags import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest @@ -295,6 +298,15 @@ private fun SearchTextField( searchBarViewModel: SearchBarViewModel, onTextChanges: (String) -> Unit, ) { + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + launch { + delay(100) + focusRequester.requestFocus() + } + } + Row( modifier = Modifier.padding(10.dp).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -312,7 +324,11 @@ private fun SearchTextField( capitalization = KeyboardCapitalization.Sentences, ), leadingIcon = { SearchIcon(modifier = Size20Modifier, Color.Unspecified) }, - modifier = Modifier.weight(1f, true).defaultMinSize(minHeight = 20.dp), + modifier = + Modifier + .weight(1f, true) + .defaultMinSize(minHeight = 20.dp) + .focusRequester(focusRequester), placeholder = { Text( text = stringResource(R.string.npub_hex_username), From 51335e06f1ddeac6c3a52fa5ee8a0645be2b942c Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 16 May 2024 13:52:22 +0200 Subject: [PATCH 20/28] Fix order of Events in DVM feeds --- .../amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index 82398916b..6a8241713 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -25,6 +25,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -63,14 +64,16 @@ open class NIP90ContentDiscoveryFilter( var eventContent = note.event?.content() - var collection: HashSet = hashSetOf() + var collection: MutableSet = mutableSetOf() val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) var json = mapper.readValue(eventContent, Array::class.java) for (element in json) { // var test = mapper.readValue(element.toString(), Array::class.java) // TODO. This is ugly. how to Kotlin? + var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() var note = LocalCache.checkGetOrCreateNote(id) + if (note != null) { collection.add(note) } @@ -78,7 +81,7 @@ open class NIP90ContentDiscoveryFilter( return collection.toList() } else { - return sort(notes) + return listOf() } } @@ -112,7 +115,7 @@ open class NIP90ContentDiscoveryFilter( var eventContent = note.event?.content() println(eventContent) - val collection: HashSet = hashSetOf() + val collection: MutableSet = mutableSetOf() val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) var json = mapper.readValue(eventContent, Array::class.java) for (element in json) { @@ -122,12 +125,13 @@ open class NIP90ContentDiscoveryFilter( val note = LocalCache.checkGetOrCreateNote(id) if (note != null) { + println(note.id().toHexKey()) collection.add(note) } } return collection } else { - return notes + return hashSetOf() } } From 7f2b8519f3f132361ff51329f128db0e69e478a0 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 16 May 2024 14:07:11 +0200 Subject: [PATCH 21/28] improve parsing --- .../ui/dal/NIP90ContentDiscoveryFilter.kt | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt index 6a8241713..0fc4cc2a9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NIP90ContentDiscoveryFilter.kt @@ -25,7 +25,6 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent import com.vitorpamplona.quartz.events.PeopleListEvent @@ -66,14 +65,10 @@ open class NIP90ContentDiscoveryFilter( var collection: MutableSet = mutableSetOf() val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - var json = mapper.readValue(eventContent, Array::class.java) - for (element in json) { - // var test = mapper.readValue(element.toString(), Array::class.java) - // TODO. This is ugly. how to Kotlin? - - var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() - var note = LocalCache.checkGetOrCreateNote(id) - + var etags = mapper.readValue(eventContent, List::class.java) + for (element in etags) { + var tag = mapper.readValue(mapper.writeValueAsString(element), Array::class.java) + val note = LocalCache.checkGetOrCreateNote(tag[1].toString()) if (note != null) { collection.add(note) } @@ -113,19 +108,15 @@ open class NIP90ContentDiscoveryFilter( var note = sorted.first() var eventContent = note.event?.content() - println(eventContent) + // println(eventContent) val collection: MutableSet = mutableSetOf() val mapper = jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - var json = mapper.readValue(eventContent, Array::class.java) - for (element in json) { - // var test = mapper.readValue(element.toString(), Array::class.java) - // TODO. This is ugly. how to Kotlin? - var id = element.toString().trimStart('[').trimStart('e').trimStart(',').trimEnd(']').trimStart().trimEnd() - - val note = LocalCache.checkGetOrCreateNote(id) + var etags = mapper.readValue(eventContent, Array::class.java) + for (element in etags) { + var tag = mapper.readValue(mapper.writeValueAsString(element), Array::class.java) + val note = LocalCache.checkGetOrCreateNote(tag[1].toString()) if (note != null) { - println(note.id().toHexKey()) collection.add(note) } } From b74fa975ba0c3ccb28162b3e3e3aea02478071d7 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 16 May 2024 14:24:13 +0200 Subject: [PATCH 22/28] Show DVM Status Limitation: This will only show the first reply from the DVM, if mutliple status updates follow they will be ignored. --- .../loggedIn/NIP90ContentDiscoveryScreen.kt | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index 66a40016f..d2b42e677 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -122,26 +122,27 @@ fun RenderNostrNIP90ContentDiscoveryScreen( thread.join() } - // TODO 2 Get the latest event from the statusFeedViewModel - // TODO How do we extract the latest event.content (or event.status) from statusFeedViewModel - // TODO We want to update dvmStatus with the content of the latest Status event + // TODO this shows the status but there might be a better way var dvmStatus = "DVM is processing..." + val thread = + Thread { + println(dvmStatus) + while (resultFeedViewModel.localFilter.feed().isEmpty()) { + try { + if (statusFeedViewModel.localFilter.feed().isNotEmpty()) { + statusFeedViewModel.localFilter.feed()[0].event?.let { dvmStatus = it.content() } + println(dvmStatus) + break + } else { + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } - /* if (statusFeedViewModel.localFilter.feed().isNotEmpty()) { - statusFeedViewModel.localFilter.feed()[0].event?.let { Text(text = it.content()) } - } else { - Text(text = "Nah") - } - - DVMStatusView( - statusFeedViewModel, - null, - enablePullRefresh = false, - accountViewModel = accountViewModel, - nav = nav, - )*/ - - // Text(text = dvminfo) + thread.start() + thread.join() HorizontalPager(state = pagerState) { RefresheableBox(resultFeedViewModel, false) { From 5bc6da3bfc53f0a716401fef092f38e5a0fc405c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 16 May 2024 13:00:51 +0000 Subject: [PATCH 23/28] New Crowdin translations by GitHub Action --- app/src/main/res/values-fr/strings.xml | 5 + app/src/main/res/values-pl-rPL/strings.xml | 188 +++++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index dc2434451..7b61dc44b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -429,6 +429,7 @@ Se déconnecter supprime toutes vos informations locales. Assurez-vous d\'avoir vos clés privées sauvegardées pour éviter de perdre votre compte. Voulez-vous continuer ? Tags suivis Relais + Découverte de notes Place de Marché Direct Communauté @@ -669,6 +670,10 @@ Afficher npub en tant que QR code Adresse invalide Amethyst a reçu une URI à ouvrir mais cette URI était invalide : %1$s + Configurer vos relais de messagerie privée + Ce paramètre informe tout le monde les relais à utiliser pour vous envoyer des messages. Sans eux, vous risquez de manquer certains messages. + Les relais de messagerie DM acceptent n\'importe quel message de n\'importe qui, mais vous permet seulement de les télécharger. Par exemple, inbox.nostr.wine fonctionne de cette façon. + Configurer maintenant Zap les Devs ! Votre don nous aide à faire la différence. Chaque sat compte ! Faire un don maintenant diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index 6b02d4f8a..c64d825b1 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -19,32 +19,80 @@ Ikona retransmitera Autor nieznany Skopiuj tekst + Kopiuj identyfikator autora + Kopiuj identyfikator notatki Zablokuj / Zgłoś Zgłoś spam/oszustwo Zgłoś podszywanie się Zgłoś niedozwoloną zawartość + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc odpowiedzieć + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby zwiększyć liczbę postów + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby polubić posty + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc wysyłać zapy + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc obserwować + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc przestać obserwować + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc ukryć słowo lub zdanie + Używasz klucza publicznego, a klucze publiczne są tylko do odczytu. Zaloguj się za pomocą klucza prywatnego, aby móc pokazać słowo lub zdanie + Zapy + Liczba wyświetleń + Zwiększ + zwiększony + edytowano + edytuj #%1$s + oryginalny + Nowa kwota w Satsach Dodaj + " i " + "na kanale " + Baner profilu Płatność zakończona pomyślnie + Błąd podczas analizowania komunikatu o błędzie + " Obserwowani" + " Obserwujący" + Profil + Filtry bezpieczeństwa Wyloguj się + Pokaż Więcej Zapłać + Notatka dla odbiorcy Dziękuję bardzo! + Kwota w Satsach + Wyślij Satsy + "Błąd analizowania podglądu dla %1$s : %2$s" + "Podgląd obrazu karty dla %1$s" Nowy kanał Nazwa kanału Moja wspaniała Grupa Adres URL zdjęcia + Opis "O nas. " Co masz na myśli? + Wpis Zapisz Utwórz Anuluj Adres retransmitera + Wpisy + Błędy Dodaj Retransmiter Nazwa użytkownika + O mnie + URL awatara + URL banera Adres URL strony Dodaj zdjęcie + Zablokowani użytkownicy + Notatki + Odpowiedzi + "Zgłoszenia" Więcej opcji " Retransmitery" + Strona www + Wyślij bezpośrednią wiadomość + Edytowanie metadanych użytkowników + Obserwuj + Również obserwuj Odblokuj Kopiuj ID użytkownika Odblokuj użytkownika @@ -52,13 +100,16 @@ Wyczyść Logo aplikacji nsec. lub npub. + hasło, aby otworzyć klucz Pokaż hasło Ukryj hasło Nieprawidłowy klucz + Nieprawidłowy klucz: %1$s "Akceptuję " warunki użytkowania Wymagane jest zaakceptowanie warunków użytkowania Hasło jest wymagane + Klucz jest wymagany Zaloguj się Zarejestruj się Utwórz konto @@ -75,18 +126,65 @@ Przestań obserwować Czat Publiczny Usuń + do + Adres Nostr + nigdy + teraz + godz. + Nagość + Wulgaryzmy / Mowa nienawiści + Zgłoś nienawistną mowę + Zgłoś Nagość / Pornografię + inne + Oznacz wszystkie jako przeczytane + Błąd Zaznacz tekst Dodaj nowe konto Konta Wybierz Konto Dodaj konto + Tylko do odczytu, brak klucza prywatnego Wstecz Wybierz + Udostępnij + ID autora + ID notatki Skopiuj tekst Usuń Przestań obserwować Śledź + Poproś o usunięcie + Amethyst poprosi o usunięcie Twojej notatki z aktualnie podłączonych retransmitorów. Nie ma gwarancji, że Twoja notatka zostanie trwale usunięta z tych retransmitorów lub z innych retransmitorów, gdzie może być przechowywana. + Zablokuj + Usuń + Zablokuj + Zgłoś + Usuń Nie pokazuj ponownie + Spam lub oszustwa + Wulgaryzmy lub nienawistne zachowanie + Złośliwe podszywanie się + Nagość lub zawartość graficzna + Nielegalne zachowanie + Zablokowanie użytkownika ukryje jego zawartość w aplikacji. Twoje notatki są nadal widoczne publicznie, w tym dla osób, które blokujesz. Zablokowani użytkownicy są wymienieni na ekranie filtrów bezpieczeństwa. + + Zgłoś nadużycie + Dodatkowe informacje + Powód + Wybierz powód… + Wyślij zgłoszenie + Zablokuj i zgłoś + Zablokuj + Zakładki + Projekty + Prywatne Zakładki + Publiczne zakładki + Dodaj do prywatnych zakładek + Dodaj do publicznych zakładek + Usuń z prywatnych zakładek + Usuń z publicznych zakładek + Pokaż tajny klucz + (0–100)% Dodaj wiadomość publiczną Dodaj prywatną wiadomość Dziękujemy za całą twoją pracę! @@ -97,13 +195,103 @@ Nadawca i odbiorca mogą zobaczyć się nawzajem i przeczytać wiadomość Twoje retransmitery (NIP-95) Pliki są przechowywane przez Twoje retransmitery. Nowy NIP: sprawdź, czy jest obsługiwany + Twoje dane zostaną natychmiast przekazane w ramach zwykłej sieci + Tak + Nie + Lista obserwowanych + Nieprawidłowy numer portu + Prywatne Wiadomości + Powiadamia Cię, gdy nadejdzie prywatna wiadomość + Otrzymano Zapy + Powiadamia Cię, gdy ktoś prześle ci zapy + Dołącz do rozmowy + ID Użytkownika lub Grupy + npub, nevent lub hex + Utwórz + Dołącz + Dzisiaj + Ostrzeżenie o zawartości + Ten post zawiera wrażliwe treści, które niektóre osoby mogą uznać za obraźliwe lub niepokojące + Zawsze ukrywaj wrażliwe treści + Zawsze pokazuj wrażliwą zawartość + Zawsze pokazuj ostrzeżenia dotyczące zawartości + Filtrowanie spamu od nieznajomych + Ostrzegaj, gdy posty zostały zgłoszone przez osoby które obserwujesz Odczytaj z Retransmitera Zapisz do Retransmitera Wystąpił błąd podczas próby uzyskania informacji o retransmiterze z %1$s + Właściciel + Wersja + Oprogramowanie + Kontakt + Adres URL płatności + Ograniczenia + Kraje + Języki + Polityka publikowania + Długość wiadomości + Subskrybcje + Filtry Retransmitery + Społeczność + Czaty + Zatwierdzone posty + Ta grupa nie ma opisu ani reguł. Porozmawiaj z właścicielem, aby je dodać + Ta społeczność nie ma opisu. Porozmawiaj z właścicielem, aby go dodać + Treść wrażliwa + Dodaje ostrzeżenie o wrażliwej treści przed wyświetleniem tej zawartości + Ustawienia + Zawsze + Tylko WiFi + Nigdy + Język + Motyw + Podgląd obrazu + Odtwarzanie wideo + Podgląd URL + Załaduj obraz + Spamerzy + Wyciszone. Kliknij, aby wyłączyć wyciszenie + Dźwięk włączony. Kliknij, aby wyciszyć + Adres Nostr został zweryfikowany + Nieudana weryfikacja adresu Nostr + Sprawdzanie adresu Nostr + Zaznacz/Odznacz wszystko Wybierz retransmiter, aby kontynuować + Przekaż zapy do: + Do + Temat + Temat dyskusji + "\@Użytkownik1, @Użytkownik2, @Użytkownik3" + Członkowie tej grupy + Kopiuj do schowka + Kopiuj npub do schowka + Kopiuj adres URL do schowka + Kopiuj ID notatki do schowka + Zaktualizuj status + Błąd podczas analizowania komunikatu o błędzie + 25 + Zdjęcie profilowe + Pokaż zdjęcia profilowe + Wybierz opcję Nie można pobrać dokumentu retransmitera + iPhone 13 + Cena (w Satach) + Miasto, Województwo, Kraj + Akcesoria + Elektronika + Meble + Książki + Zwierzęta domowe + Sporty + Fitness + Sztuka + Biuro Retransmiter %1$s Rozwiń listę retransmiterów Wybór listy retransmiterów + Nieprawidłowy adres + Skonfiguruj teraz + Dziękuję! + Maksymalny Limit From 926a721c533a816c9ac062dff67fbd536d2f385c Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 16 May 2024 15:02:44 +0200 Subject: [PATCH 24/28] cleanup, potential fix endless loop --- .../loggedIn/NIP90ContentDiscoveryScreen.kt | 40 ++++++------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index d2b42e677..f2f5a4f7f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -40,7 +40,6 @@ import com.vitorpamplona.amethyst.ui.screen.NostrNIP90StatusFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableBox import com.vitorpamplona.amethyst.ui.screen.RenderFeedState import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState -import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryRequestEvent @Composable @@ -90,7 +89,7 @@ fun NIP90ContentDiscoveryScreen( @Composable @OptIn(ExperimentalFoundationApi::class) fun RenderNostrNIP90ContentDiscoveryScreen( - DVMID: String?, + dvmID: String?, accountViewModel: AccountViewModel, nav: (String) -> Unit, resultFeedViewModel: NostrNIP90ContentDiscoveryFeedViewModel, @@ -99,51 +98,37 @@ fun RenderNostrNIP90ContentDiscoveryScreen( Column(Modifier.fillMaxHeight()) { val pagerState = rememberPagerState { 2 } val coroutineScope = rememberCoroutineScope() - // TODO 1 Render a nice header with image and DVM name from the id - // TODO How do we get the event information here? - var dvminfo = "DVM " + DVMID - if (DVMID != null) { - val thread = - Thread { - try { - var note = LocalCache.checkGetOrCreateNote(DVMID) - if (note != null) { - dvminfo = ((note.event as AppDefinitionEvent).appMetaData()?.name ?: "DVM from note") - } else { - dvminfo = "DVM from not found" - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - thread.start() - thread.join() - } - - // TODO this shows the status but there might be a better way + // TODO this now shows the first status update but there might be a better way var dvmStatus = "DVM is processing..." val thread = Thread { - println(dvmStatus) + var count = 0 while (resultFeedViewModel.localFilter.feed().isEmpty()) { try { if (statusFeedViewModel.localFilter.feed().isNotEmpty()) { statusFeedViewModel.localFilter.feed()[0].event?.let { dvmStatus = it.content() } println(dvmStatus) break + } else if (count > 1000) { + // Might not be the best way, but we want to avoid hanging in the loop forever + break } else { + count++ } } catch (e: Exception) { e.printStackTrace() } } } - thread.start() thread.join() + // TODO Maybe render a nice header with image and DVM name from the dvmID + // TODO How do we get the event information here?, LocalCache.checkGetOrCreateNote() returns note but event is empty + // TODO oterwise we have the NIP89 info in (note.event as AppDefinitionEvent).appMetaData() + // Text(text = dvminfo) + HorizontalPager(state = pagerState) { RefresheableBox(resultFeedViewModel, false) { SaveableFeedState(resultFeedViewModel, null) { listState -> @@ -154,6 +139,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( nav, null, onEmpty = { + // TODO Maybe also show some dvm image/text while waiting for the notes in this custom component FeedEmptywithStatus(status = dvmStatus) { } }, From 6bd98201f85b3733afd656c7e7038f48a35a86de Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 16 May 2024 15:16:30 +0200 Subject: [PATCH 25/28] add DVM not replying message --- .../ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt | 6 ++++-- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index f2f5a4f7f..eddc12ce6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -31,7 +31,9 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.ui.screen.FeedEmptywithStatus @@ -100,7 +102,8 @@ fun RenderNostrNIP90ContentDiscoveryScreen( val coroutineScope = rememberCoroutineScope() // TODO this now shows the first status update but there might be a better way - var dvmStatus = "DVM is processing..." + var dvmStatus = stringResource(R.string.dvm_no_status) + val thread = Thread { var count = 0 @@ -112,7 +115,6 @@ fun RenderNostrNIP90ContentDiscoveryScreen( break } else if (count > 1000) { // Might not be the best way, but we want to avoid hanging in the loop forever - break } else { count++ } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 581930251..89980e2af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -846,4 +846,6 @@ Draft Note From Msg + + DVM seems not to reply From 193e9a5adbeb52661424ceccc95dc33844361ff3 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 16 May 2024 15:19:19 +0200 Subject: [PATCH 26/28] add more info on current dvm state --- .../ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt | 8 +++++--- app/src/main/res/values/strings.xml | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index eddc12ce6..0c732212d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -102,7 +102,8 @@ fun RenderNostrNIP90ContentDiscoveryScreen( val coroutineScope = rememberCoroutineScope() // TODO this now shows the first status update but there might be a better way - var dvmStatus = stringResource(R.string.dvm_no_status) + var dvmState = stringResource(R.string.dvm_waiting_status) + var dvmNoState = stringResource(R.string.dvm_no_status) val thread = Thread { @@ -111,9 +112,10 @@ fun RenderNostrNIP90ContentDiscoveryScreen( try { if (statusFeedViewModel.localFilter.feed().isNotEmpty()) { statusFeedViewModel.localFilter.feed()[0].event?.let { dvmStatus = it.content() } - println(dvmStatus) + println(dvmState) break } else if (count > 1000) { + dvmState = dvmNoState // Might not be the best way, but we want to avoid hanging in the loop forever } else { count++ @@ -142,7 +144,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( null, onEmpty = { // TODO Maybe also show some dvm image/text while waiting for the notes in this custom component - FeedEmptywithStatus(status = dvmStatus) { + FeedEmptywithStatus(status = dvmState) { } }, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 89980e2af..2cf9242a4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -847,5 +847,6 @@ From Msg + Waiting for DVM to reply DVM seems not to reply From c494cf8ac10011d8932c4ba5266ac196fe5980c1 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 16 May 2024 15:20:04 +0200 Subject: [PATCH 27/28] Update NIP90ContentDiscoveryScreen.kt --- .../amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index 0c732212d..13dc1d2cb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -111,7 +111,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( while (resultFeedViewModel.localFilter.feed().isEmpty()) { try { if (statusFeedViewModel.localFilter.feed().isNotEmpty()) { - statusFeedViewModel.localFilter.feed()[0].event?.let { dvmStatus = it.content() } + statusFeedViewModel.localFilter.feed()[0].event?.let { dvmState = it.content() } println(dvmState) break } else if (count > 1000) { From b2193f48d5ebe16878ea636c867f30d7f9327c49 Mon Sep 17 00:00:00 2001 From: Believethehype <1097224+believethehype@users.noreply.github.com> Date: Thu, 16 May 2024 16:48:19 +0200 Subject: [PATCH 28/28] Update NIP90ContentDiscoveryScreen.kt --- .../ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt index 13dc1d2cb..5c0121309 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NIP90ContentDiscoveryScreen.kt @@ -101,7 +101,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( val pagerState = rememberPagerState { 2 } val coroutineScope = rememberCoroutineScope() - // TODO this now shows the first status update but there might be a better way + // TODO (Optional) this now shows the first status update but there might be a better way var dvmState = stringResource(R.string.dvm_waiting_status) var dvmNoState = stringResource(R.string.dvm_no_status) @@ -128,14 +128,15 @@ fun RenderNostrNIP90ContentDiscoveryScreen( thread.start() thread.join() - // TODO Maybe render a nice header with image and DVM name from the dvmID - // TODO How do we get the event information here?, LocalCache.checkGetOrCreateNote() returns note but event is empty - // TODO oterwise we have the NIP89 info in (note.event as AppDefinitionEvent).appMetaData() + // TODO (Optional) Maybe render a nice header with image and DVM name from the dvmID + // TODO (Optional) How do we get the event information here?, LocalCache.checkGetOrCreateNote() returns note but event is empty + // TODO (Optional) otherwise we have the NIP89 info in (note.event as AppDefinitionEvent).appMetaData() // Text(text = dvminfo) HorizontalPager(state = pagerState) { RefresheableBox(resultFeedViewModel, false) { SaveableFeedState(resultFeedViewModel, null) { listState -> + // TODO (Optional) Instead of a like reaction, do a Kind 31989 NIP89 App recommendation RenderFeedState( resultFeedViewModel, accountViewModel, @@ -143,7 +144,7 @@ fun RenderNostrNIP90ContentDiscoveryScreen( nav, null, onEmpty = { - // TODO Maybe also show some dvm image/text while waiting for the notes in this custom component + // TODO (Optional) Maybe also show some dvm image/text while waiting for the notes in this custom component FeedEmptywithStatus(status = dvmState) { } },